This is page 20 of 23. Use http://codebase.md/tosin2013/documcp?lines=false&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/memory/export-import.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Memory Export/Import System for DocuMCP
* Comprehensive data portability, backup, and migration capabilities
*/
import { EventEmitter } from "events";
import { promises as fs } from "fs";
import { createWriteStream } from "fs";
import { MemoryEntry, JSONLStorage } from "./storage.js";
import { MemoryManager } from "./manager.js";
import { IncrementalLearningSystem } from "./learning.js";
import { KnowledgeGraph } from "./knowledge-graph.js";
import { MemoryPruningSystem } from "./pruning.js";
export interface ExportOptions {
format: "json" | "jsonl" | "csv" | "xml" | "yaml" | "sqlite" | "archive";
compression?: "gzip" | "zip" | "none";
includeMetadata: boolean;
includeLearning: boolean;
includeKnowledgeGraph: boolean;
filters?: {
types?: string[];
dateRange?: { start: Date; end: Date };
projects?: string[];
tags?: string[];
outcomes?: string[];
};
anonymize?: {
enabled: boolean;
fields: string[];
method: "hash" | "remove" | "pseudonymize";
};
encryption?: {
enabled: boolean;
algorithm: "aes-256-gcm" | "aes-192-gcm" | "aes-128-gcm";
password?: string;
};
}
export interface ImportOptions {
format: "json" | "jsonl" | "csv" | "xml" | "yaml" | "sqlite" | "archive";
mode: "merge" | "replace" | "append" | "update";
validation: "strict" | "loose" | "none";
conflictResolution: "skip" | "overwrite" | "merge" | "rename";
backup: boolean;
dryRun: boolean;
mapping?: Record<string, string>; // Field mapping for different schemas
transformation?: {
enabled: boolean;
rules: Array<{
field: string;
operation: "convert" | "transform" | "validate";
params: any;
}>;
};
}
export interface ExportResult {
success: boolean;
filePath?: string;
format: string;
size: number;
entries: number;
metadata: {
exportedAt: Date;
version: string;
source: string;
includes: string[];
compression?: string;
encryption?: boolean;
};
warnings: string[];
errors: string[];
}
export interface ImportResult {
success: boolean;
processed: number;
imported: number;
skipped: number;
errors: number;
errorDetails: string[]; // Detailed error messages
conflicts: number;
validation: {
valid: number;
invalid: number;
warnings: string[];
};
summary: {
newEntries: number;
updatedEntries: number;
duplicateEntries: number;
failedEntries: number;
};
metadata: {
importedAt: Date;
source: string;
format: string;
mode: string;
};
}
export interface MigrationPlan {
sourceSystem: string;
targetSystem: string;
mapping: Record<string, string>;
transformations: Array<{
field: string;
type: "rename" | "convert" | "merge" | "split" | "calculate";
source: string | string[];
target: string;
operation?: string;
}>;
validation: Array<{
field: string;
rules: string[];
required: boolean;
}>;
postProcessing: string[];
}
export interface ArchiveMetadata {
version: string;
created: Date;
source: string;
description: string;
manifest: {
files: Array<{
name: string;
type: string;
size: number;
checksum: string;
entries?: number;
}>;
total: {
files: number;
size: number;
entries: number;
};
};
options: ExportOptions;
}
export class MemoryExportImportSystem extends EventEmitter {
private storage: JSONLStorage;
private manager: MemoryManager;
private learningSystem: IncrementalLearningSystem;
private knowledgeGraph: KnowledgeGraph;
private pruningSystem?: MemoryPruningSystem;
private readonly version = "1.0.0";
constructor(
storage: JSONLStorage,
manager: MemoryManager,
learningSystem: IncrementalLearningSystem,
knowledgeGraph: KnowledgeGraph,
pruningSystem?: MemoryPruningSystem,
) {
super();
this.storage = storage;
this.manager = manager;
this.learningSystem = learningSystem;
this.knowledgeGraph = knowledgeGraph;
this.pruningSystem = pruningSystem;
}
/**
* Export memory data to specified format
*/
async exportMemories(
outputPath: string,
options: Partial<ExportOptions> = {},
): Promise<ExportResult> {
const defaultOptions: ExportOptions = {
format: "json",
compression: "none",
includeMetadata: true,
includeLearning: true,
includeKnowledgeGraph: true,
anonymize: {
enabled: false,
fields: ["userId", "email", "personalInfo"],
method: "hash",
},
encryption: {
enabled: false,
algorithm: "aes-256-gcm",
},
};
const activeOptions = { ...defaultOptions, ...options };
const startTime = Date.now();
this.emit("export_started", { outputPath, options: activeOptions });
try {
// Get filtered entries
const entries = await this.getFilteredEntries(activeOptions.filters);
// Prepare export data
const exportData = await this.prepareExportData(entries, activeOptions);
// Apply anonymization if enabled
if (activeOptions.anonymize?.enabled) {
this.applyAnonymization(exportData, activeOptions.anonymize);
}
// Prepare output path - if compression is requested, use temp file first
let actualOutputPath = outputPath;
if (activeOptions.compression && activeOptions.compression !== "none") {
// For compressed exports, export to temp file first
if (outputPath.endsWith(".gz")) {
actualOutputPath = outputPath.slice(0, -3); // Remove .gz suffix
} else if (outputPath.endsWith(".zip")) {
actualOutputPath = outputPath.slice(0, -4); // Remove .zip suffix
}
}
// Export to specified format
let filePath: string;
let size = 0;
switch (activeOptions.format) {
case "json":
filePath = await this.exportToJSON(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "jsonl":
filePath = await this.exportToJSONL(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "csv":
filePath = await this.exportToCSV(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "xml":
filePath = await this.exportToXML(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "yaml":
filePath = await this.exportToYAML(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "sqlite":
filePath = await this.exportToSQLite(
actualOutputPath,
exportData,
activeOptions,
);
break;
case "archive":
filePath = await this.exportToArchive(
actualOutputPath,
exportData,
activeOptions,
);
break;
default:
throw new Error(`Unsupported export format: ${activeOptions.format}`);
}
// Apply compression if specified
if (activeOptions.compression && activeOptions.compression !== "none") {
filePath = await this.applyCompression(
filePath,
activeOptions.compression,
outputPath,
);
}
// Apply encryption if enabled
if (activeOptions.encryption?.enabled) {
filePath = await this.applyEncryption(
filePath,
activeOptions.encryption,
);
}
// Get file size
const stats = await fs.stat(filePath);
size = stats.size;
const result: ExportResult = {
success: true,
filePath,
format: activeOptions.format,
size,
entries: entries.length,
metadata: {
exportedAt: new Date(),
version: this.version,
source: "DocuMCP Memory System",
includes: this.getIncludedComponents(activeOptions),
compression:
activeOptions.compression !== "none"
? activeOptions.compression
: undefined,
encryption: activeOptions.encryption?.enabled,
},
warnings: [],
errors: [],
};
this.emit("export_completed", {
result,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.emit("export_error", { error: errorMessage });
return {
success: false,
format: activeOptions.format,
size: 0,
entries: 0,
metadata: {
exportedAt: new Date(),
version: this.version,
source: "DocuMCP Memory System",
includes: [],
},
warnings: [],
errors: [errorMessage],
};
}
}
/**
* Import memory data from specified source
*/
async importMemories(
inputPath: string,
options: Partial<ImportOptions> = {},
): Promise<ImportResult> {
const defaultOptions: ImportOptions = {
format: "json",
mode: "merge",
validation: "strict",
conflictResolution: "skip",
backup: true,
dryRun: false,
};
const activeOptions = { ...defaultOptions, ...options };
const startTime = Date.now();
this.emit("import_started", { inputPath, options: activeOptions });
try {
// Create backup if requested
if (activeOptions.backup && !activeOptions.dryRun) {
await this.createBackup();
}
// Detect and verify format
const detectedFormat = await this.detectFormat(inputPath);
if (detectedFormat !== activeOptions.format) {
this.emit("format_mismatch", {
detected: detectedFormat,
specified: activeOptions.format,
});
}
// Load and parse import data
const importData = await this.loadImportData(inputPath, activeOptions);
// Validate import data
const validationResult = await this.validateImportData(
importData,
activeOptions,
);
if (
validationResult.invalid > 0 &&
activeOptions.validation === "strict"
) {
throw new Error(
`Validation failed: ${validationResult.invalid} invalid entries`,
);
}
// Process import data
const result = await this.processImportData(importData, activeOptions);
this.emit("import_completed", {
result,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.emit("import_error", { error: errorMessage });
return {
success: false,
processed: 0,
imported: 0,
skipped: 0,
errors: 1,
errorDetails: [errorMessage],
conflicts: 0,
validation: {
valid: 0,
invalid: 0,
warnings: [],
},
summary: {
newEntries: 0,
updatedEntries: 0,
duplicateEntries: 0,
failedEntries: 0,
},
metadata: {
importedAt: new Date(),
source: inputPath,
format: activeOptions.format,
mode: activeOptions.mode,
},
};
}
}
/**
* Create migration plan between different systems
*/
async createMigrationPlan(
sourceSchema: any,
targetSchema: any,
options?: {
autoMap?: boolean;
preserveStructure?: boolean;
customMappings?: Record<string, string>;
},
): Promise<MigrationPlan> {
const plan: MigrationPlan = {
sourceSystem: sourceSchema.system || "Unknown",
targetSystem: "DocuMCP",
mapping: {},
transformations: [],
validation: [],
postProcessing: [],
};
// Auto-generate field mappings
if (options?.autoMap !== false) {
plan.mapping = this.generateFieldMappings(sourceSchema, targetSchema);
}
// Apply custom mappings
if (options?.customMappings) {
Object.assign(plan.mapping, options.customMappings);
}
// Generate transformations
plan.transformations = this.generateTransformations(
sourceSchema,
targetSchema,
plan.mapping,
);
// Generate validation rules
plan.validation = this.generateValidationRules(targetSchema);
// Generate post-processing steps
plan.postProcessing = this.generatePostProcessingSteps(targetSchema);
return plan;
}
/**
* Execute migration plan
*/
async executeMigration(
inputPath: string,
migrationPlan: MigrationPlan,
options?: Partial<ImportOptions>,
): Promise<ImportResult> {
this.emit("migration_started", { inputPath, plan: migrationPlan });
try {
// Load source data
const sourceData = await this.loadRawData(inputPath);
// Apply transformations
const transformedData = await this.applyTransformations(
sourceData,
migrationPlan,
);
// Convert to import format
const importData = this.convertToImportFormat(
transformedData,
migrationPlan,
);
// Execute import with migration settings
const importOptions: ImportOptions = {
format: "json",
mode: "merge",
validation: "strict",
conflictResolution: "merge",
backup: true,
dryRun: false,
...options,
transformation: {
enabled: true,
rules: migrationPlan.transformations.map((t) => ({
field: t.target,
operation: t.type as any,
params: { source: t.source, operation: t.operation },
})),
},
};
const result = await this.processImportData(importData, importOptions);
// Execute post-processing
if (migrationPlan.postProcessing.length > 0) {
await this.executePostProcessing(migrationPlan.postProcessing);
}
this.emit("migration_completed", { result });
return result;
} catch (error) {
this.emit("migration_error", {
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Get supported formats
*/
getSupportedFormats(): {
export: string[];
import: string[];
compression: string[];
encryption: string[];
} {
return {
export: ["json", "jsonl", "csv", "xml", "yaml", "sqlite", "archive"],
import: ["json", "jsonl", "csv", "xml", "yaml", "sqlite", "archive"],
compression: ["gzip", "zip", "none"],
encryption: ["aes-256-gcm", "aes-192-gcm", "aes-128-gcm"],
};
}
/**
* Validate export/import compatibility
*/
async validateCompatibility(
sourcePath: string,
_targetSystem: string = "DocuMCP",
): Promise<{
compatible: boolean;
issues: string[];
recommendations: string[];
migrationRequired: boolean;
}> {
try {
const format = await this.detectFormat(sourcePath);
const sampleData = await this.loadSampleData(sourcePath, format);
const issues: string[] = [];
const recommendations: string[] = [];
let compatible = true;
let migrationRequired = false;
// Check format compatibility
if (!this.getSupportedFormats().import.includes(format)) {
issues.push(`Unsupported format: ${format}`);
compatible = false;
}
// Check schema compatibility
const schemaIssues = this.validateSchema(sampleData);
if (schemaIssues.length > 0) {
issues.push(...schemaIssues);
migrationRequired = true;
}
// Check data integrity
const integrityIssues = this.validateDataIntegrity(sampleData);
if (integrityIssues.length > 0) {
issues.push(...integrityIssues);
recommendations.push("Consider data cleaning before import");
}
// Generate recommendations
if (migrationRequired) {
recommendations.push("Create migration plan for schema transformation");
}
if (format === "csv") {
recommendations.push(
"Consider using JSON or JSONL for better data preservation",
);
}
return {
compatible,
issues,
recommendations,
migrationRequired,
};
} catch (error) {
return {
compatible: false,
issues: [error instanceof Error ? error.message : String(error)],
recommendations: ["Verify file format and accessibility"],
migrationRequired: false,
};
}
}
/**
* Private helper methods
*/
private async getFilteredEntries(
filters?: ExportOptions["filters"],
): Promise<MemoryEntry[]> {
let entries = await this.storage.getAll();
if (!filters) return entries;
// Apply type filter
if (filters.types && filters.types.length > 0) {
entries = entries.filter((entry) => filters.types!.includes(entry.type));
}
// Apply date range filter
if (filters.dateRange) {
entries = entries.filter((entry) => {
const entryDate = new Date(entry.timestamp);
return (
entryDate >= filters.dateRange!.start &&
entryDate <= filters.dateRange!.end
);
});
}
// Apply project filter
if (filters.projects && filters.projects.length > 0) {
entries = entries.filter((entry) =>
filters.projects!.some(
(project) =>
entry.data.projectPath?.includes(project) ||
entry.data.projectId === project,
),
);
}
// Apply tags filter
if (filters.tags && filters.tags.length > 0) {
entries = entries.filter(
(entry) => entry.tags?.some((tag) => filters.tags!.includes(tag)),
);
}
// Apply outcomes filter
if (filters.outcomes && filters.outcomes.length > 0) {
entries = entries.filter(
(entry) =>
filters.outcomes!.includes(entry.data.outcome) ||
(entry.data.success === true &&
filters.outcomes!.includes("success")) ||
(entry.data.success === false &&
filters.outcomes!.includes("failure")),
);
}
return entries;
}
private async prepareExportData(
entries: MemoryEntry[],
options: ExportOptions,
): Promise<any> {
const exportData: any = {
metadata: {
version: this.version,
exportedAt: new Date().toISOString(),
source: "DocuMCP Memory System",
entries: entries.length,
options: {
includeMetadata: options.includeMetadata,
includeLearning: options.includeLearning,
includeKnowledgeGraph: options.includeKnowledgeGraph,
},
},
memories: entries,
};
// Include learning data if requested
if (options.includeLearning) {
const patterns = await this.learningSystem.getPatterns();
exportData.learning = {
patterns,
statistics: await this.learningSystem.getStatistics(),
};
}
// Include knowledge graph if requested
if (options.includeKnowledgeGraph) {
const nodes = await this.knowledgeGraph.getAllNodes();
const edges = await this.knowledgeGraph.getAllEdges();
exportData.knowledgeGraph = {
nodes,
edges,
statistics: await this.knowledgeGraph.getStatistics(),
};
}
return exportData;
}
private applyAnonymization(
data: any,
anonymizeOptions: { fields: string[]; method: string },
): void {
const anonymizeValue = (value: any, method: string): any => {
if (typeof value !== "string") return value;
switch (method) {
case "hash":
return this.hashValue(value);
case "remove":
return null;
case "pseudonymize":
return this.pseudonymizeValue(value);
default:
return value;
}
};
const anonymizeObject = (obj: any): void => {
for (const [key, value] of Object.entries(obj)) {
if (anonymizeOptions.fields.includes(key)) {
obj[key] = anonymizeValue(value, anonymizeOptions.method);
} else if (typeof value === "object" && value !== null) {
anonymizeObject(value);
}
}
};
anonymizeObject(data);
}
private hashValue(value: string): string {
// Simple hash - in production, use a proper cryptographic hash
let hash = 0;
for (let i = 0; i < value.length; i++) {
const char = value.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return `hash_${Math.abs(hash).toString(36)}`;
}
private pseudonymizeValue(_value: string): string {
// Simple pseudonymization - in production, use proper techniques
const prefixes = ["user", "project", "system", "item"];
const suffix = Math.random().toString(36).substr(2, 8);
const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
return `${prefix}_${suffix}`;
}
private async exportToJSON(
outputPath: string,
data: any,
_options: ExportOptions,
): Promise<string> {
const jsonData = JSON.stringify(data, null, 2);
// Handle compression-aware file paths (e.g., file.json.gz)
let filePath = outputPath;
if (!outputPath.includes(".json")) {
filePath = `${outputPath}.json`;
}
await fs.writeFile(filePath, jsonData, "utf8");
return filePath;
}
private async exportToJSONL(
outputPath: string,
data: any,
_options: ExportOptions,
): Promise<string> {
const filePath = outputPath.endsWith(".jsonl")
? outputPath
: `${outputPath}.jsonl`;
return new Promise((resolve, reject) => {
const writeStream = createWriteStream(filePath);
writeStream.on("error", (error) => {
reject(error);
});
writeStream.on("finish", () => {
resolve(filePath);
});
// Write metadata as first line
writeStream.write(JSON.stringify(data.metadata) + "\n");
// Write each memory entry as a separate line
for (const entry of data.memories) {
writeStream.write(JSON.stringify(entry) + "\n");
}
// Write learning data if included
if (data.learning) {
writeStream.write(
JSON.stringify({ type: "learning", data: data.learning }) + "\n",
);
}
// Write knowledge graph if included
if (data.knowledgeGraph) {
writeStream.write(
JSON.stringify({
type: "knowledgeGraph",
data: data.knowledgeGraph,
}) + "\n",
);
}
writeStream.end();
});
}
private async exportToCSV(
outputPath: string,
data: any,
_options: ExportOptions,
): Promise<string> {
const filePath = outputPath.endsWith(".csv")
? outputPath
: `${outputPath}.csv`;
// Flatten memory entries for CSV format
const flattenedEntries = data.memories.map((entry: MemoryEntry) => ({
id: entry.id,
timestamp: entry.timestamp,
type: entry.type,
projectPath: entry.data.projectPath || "",
projectId: entry.data.projectId || "",
language: entry.data.language || "",
framework: entry.data.framework || "",
outcome: entry.data.outcome || "",
success: entry.data.success || false,
tags: entry.tags?.join(";") || "",
metadata: JSON.stringify(entry.metadata || {}),
}));
// Generate CSV headers
const headers = Object.keys(flattenedEntries[0] || {});
const csvLines = [headers.join(",")];
// Generate CSV rows
for (const entry of flattenedEntries) {
const row = headers.map((header) => {
const value = entry[header as keyof typeof entry];
const stringValue =
typeof value === "string" ? value : JSON.stringify(value);
return `"${stringValue.replace(/"/g, '""')}"`;
});
csvLines.push(row.join(","));
}
await fs.writeFile(filePath, csvLines.join("\n"), "utf8");
return filePath;
}
private async exportToXML(
outputPath: string,
data: any,
_options: ExportOptions,
): Promise<string> {
const filePath = outputPath.endsWith(".xml")
? outputPath
: `${outputPath}.xml`;
const xmlData = this.convertToXML(data);
await fs.writeFile(filePath, xmlData, "utf8");
return filePath;
}
private async exportToYAML(
outputPath: string,
data: any,
_options: ExportOptions,
): Promise<string> {
const filePath = outputPath.endsWith(".yaml")
? outputPath
: `${outputPath}.yaml`;
// Simple YAML conversion - in production, use a proper YAML library
const yamlData = this.convertToYAML(data);
await fs.writeFile(filePath, yamlData, "utf8");
return filePath;
}
private async exportToSQLite(
_outputPath: string,
_data: any,
_options: ExportOptions,
): Promise<string> {
// This would require a SQLite library like better-sqlite3
// For now, throw an error indicating additional dependencies needed
throw new Error(
"SQLite export requires additional dependencies (better-sqlite3)",
);
}
private async exportToArchive(
outputPath: string,
data: any,
options: ExportOptions,
): Promise<string> {
const archivePath = outputPath.endsWith(".tar")
? outputPath
: `${outputPath}.tar`;
// Create archive metadata
const metadata: ArchiveMetadata = {
version: this.version,
created: new Date(),
source: "DocuMCP Memory System",
description: "Complete memory system export archive",
manifest: {
files: [],
total: { files: 0, size: 0, entries: data.memories.length },
},
options,
};
// This would require archiving capabilities
// For now, create multiple files and reference them in metadata
const baseDir = archivePath.replace(".tar", "");
await fs.mkdir(baseDir, { recursive: true });
// Export memories as JSON
const memoriesPath = `${baseDir}/memories.json`;
await this.exportToJSON(memoriesPath, { memories: data.memories }, options);
metadata.manifest.files.push({
name: "memories.json",
type: "memories",
size: (await fs.stat(memoriesPath)).size,
checksum: "sha256-placeholder",
entries: data.memories.length,
});
// Export learning data if included
if (data.learning) {
const learningPath = `${baseDir}/learning.json`;
await this.exportToJSON(learningPath, data.learning, options);
metadata.manifest.files.push({
name: "learning.json",
type: "learning",
size: (await fs.stat(learningPath)).size,
checksum: "sha256-placeholder",
});
}
// Export knowledge graph if included
if (data.knowledgeGraph) {
const kgPath = `${baseDir}/knowledge-graph.json`;
await this.exportToJSON(kgPath, data.knowledgeGraph, options);
metadata.manifest.files.push({
name: "knowledge-graph.json",
type: "knowledge-graph",
size: (await fs.stat(kgPath)).size,
checksum: "sha256-placeholder",
});
}
// Write metadata
const metadataPath = `${baseDir}/metadata.json`;
await this.exportToJSON(metadataPath, metadata, options);
return baseDir;
}
private async applyCompression(
filePath: string,
compression: string,
targetPath?: string,
): Promise<string> {
if (compression === "gzip") {
const compressedPath = targetPath || `${filePath}.gz`;
const content = await fs.readFile(filePath, "utf8");
// Simple mock compression - just add a header and write the content
await fs.writeFile(compressedPath, `GZIP_HEADER\n${content}`, "utf8");
// Clean up temp file if we used one
if (targetPath && targetPath !== filePath) {
await fs.unlink(filePath);
}
return compressedPath;
}
// For other compression types or 'none', return original path
this.emit("compression_skipped", {
reason: "Not implemented",
compression,
});
return filePath;
}
private async applyEncryption(
filePath: string,
encryption: any,
): Promise<string> {
// This would require encryption capabilities
// For now, return the original path
this.emit("encryption_skipped", { reason: "Not implemented", encryption });
return filePath;
}
private getIncludedComponents(options: ExportOptions): string[] {
const components = ["memories"];
if (options.includeMetadata) components.push("metadata");
if (options.includeLearning) components.push("learning");
if (options.includeKnowledgeGraph) components.push("knowledge-graph");
return components;
}
private async detectFormat(filePath: string): Promise<string> {
const extension = filePath.split(".").pop()?.toLowerCase();
switch (extension) {
case "json":
return "json";
case "jsonl":
return "jsonl";
case "csv":
return "csv";
case "xml":
return "xml";
case "yaml":
case "yml":
return "yaml";
case "db":
case "sqlite":
return "sqlite";
case "tar":
case "zip":
return "archive";
default: {
// Try to detect by content
const content = await fs.readFile(filePath, "utf8");
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
return "json";
}
if (content.includes("<?xml")) {
return "xml";
}
return "unknown";
}
}
}
private async loadImportData(
filePath: string,
options: ImportOptions,
): Promise<any> {
switch (options.format) {
case "json":
return JSON.parse(await fs.readFile(filePath, "utf8"));
case "jsonl":
return this.loadJSONLData(filePath);
case "csv":
return this.loadCSVData(filePath);
case "xml":
return this.loadXMLData(filePath);
case "yaml":
return this.loadYAMLData(filePath);
default:
throw new Error(`Unsupported import format: ${options.format}`);
}
}
private async loadJSONLData(filePath: string): Promise<any> {
const content = await fs.readFile(filePath, "utf8");
const lines = content.trim().split("\n");
const data: any = { memories: [], learning: null, knowledgeGraph: null };
for (const line of lines) {
const parsed = JSON.parse(line);
if (parsed.type === "learning") {
data.learning = parsed.data;
} else if (parsed.type === "knowledgeGraph") {
data.knowledgeGraph = parsed.data;
} else if (parsed.version) {
data.metadata = parsed;
} else {
data.memories.push(parsed);
}
}
return data;
}
private async loadCSVData(filePath: string): Promise<any> {
const content = await fs.readFile(filePath, "utf8");
const lines = content.trim().split("\n");
const headers = lines[0].split(",").map((h) => h.replace(/"/g, ""));
const memories = [];
for (let i = 1; i < lines.length; i++) {
const values = this.parseCSVLine(lines[i]);
const entry: any = {};
for (let j = 0; j < headers.length; j++) {
const header = headers[j];
const value = values[j];
// Parse special fields
if (header === "tags") {
entry.tags = value ? value.split(";") : [];
} else if (header === "metadata") {
try {
entry.metadata = JSON.parse(value);
} catch {
entry.metadata = {};
}
} else if (header === "success") {
entry.data = entry.data || {};
entry.data.success = value === "true";
} else if (
[
"projectPath",
"projectId",
"language",
"framework",
"outcome",
].includes(header)
) {
entry.data = entry.data || {};
entry.data[header] = value;
} else {
entry[header] = value;
}
}
memories.push(entry);
}
return { memories };
}
private parseCSVLine(line: string): string[] {
const values: string[] = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === "," && !inQuotes) {
values.push(current);
current = "";
} else {
current += char;
}
}
values.push(current);
return values;
}
private async loadXMLData(_filePath: string): Promise<any> {
// This would require an XML parser
throw new Error("XML import requires additional dependencies (xml2js)");
}
private async loadYAMLData(_filePath: string): Promise<any> {
// This would require a YAML parser
throw new Error("YAML import requires additional dependencies (js-yaml)");
}
private async validateImportData(
data: any,
options: ImportOptions,
): Promise<{ valid: number; invalid: number; warnings: string[] }> {
const result = { valid: 0, invalid: 0, warnings: [] as string[] };
if (!data.memories || !Array.isArray(data.memories)) {
result.warnings.push("No memories array found in import data");
return result;
}
for (const entry of data.memories) {
if (this.validateMemoryEntry(entry, options.validation)) {
result.valid++;
} else {
result.invalid++;
}
}
return result;
}
private validateMemoryEntry(entry: any, validation: string): boolean {
// Check for completely missing or null required fields
if (
!entry.id ||
!entry.timestamp ||
entry.type === null ||
entry.type === undefined ||
entry.data === null
) {
return false; // These are invalid regardless of validation level
}
if (!entry.type) {
return validation !== "strict";
}
if (validation === "strict") {
return Boolean(entry.data && typeof entry.data === "object");
}
// For loose validation, still require data to be defined (not null)
if (validation === "loose" && entry.data === null) {
return false;
}
return true;
}
private async processImportData(
data: any,
options: ImportOptions,
): Promise<ImportResult> {
const result: ImportResult = {
success: true,
processed: 0,
imported: 0,
skipped: 0,
errors: 0,
errorDetails: [],
conflicts: 0,
validation: { valid: 0, invalid: 0, warnings: [] },
summary: {
newEntries: 0,
updatedEntries: 0,
duplicateEntries: 0,
failedEntries: 0,
},
metadata: {
importedAt: new Date(),
source: "imported data",
format: options.format,
mode: options.mode,
},
};
if (!data.memories || !Array.isArray(data.memories)) {
result.success = false;
result.errors = 1;
result.errorDetails = ["No valid memories array found in import data"];
return result;
}
for (const entry of data.memories) {
result.processed++;
try {
// Apply transformations and mappings
let transformedEntry = { ...entry };
if (options.mapping || options.transformation?.enabled) {
transformedEntry = this.applyDataTransformations(entry, options);
}
if (!this.validateMemoryEntry(transformedEntry, options.validation)) {
result.validation.invalid++;
result.errors++;
result.summary.failedEntries++;
result.errorDetails.push(
`Invalid memory entry: ${
transformedEntry.id || "unknown"
} - validation failed`,
);
continue;
}
result.validation.valid++;
// Check for conflicts
const existing = await this.storage.get(transformedEntry.id);
if (existing) {
result.conflicts++;
switch (options.conflictResolution) {
case "skip":
result.skipped++;
result.summary.duplicateEntries++;
continue;
case "overwrite":
if (!options.dryRun) {
await this.storage.update(
transformedEntry.id,
transformedEntry,
);
result.imported++;
result.summary.updatedEntries++;
}
break;
case "merge":
if (!options.dryRun) {
const merged = this.mergeEntries(existing, transformedEntry);
await this.storage.update(transformedEntry.id, merged);
result.imported++;
result.summary.updatedEntries++;
}
break;
case "rename": {
const newId = `${transformedEntry.id}_imported_${Date.now()}`;
if (!options.dryRun) {
await this.storage.store({ ...transformedEntry, id: newId });
result.imported++;
result.summary.newEntries++;
}
break;
}
}
} else {
if (!options.dryRun) {
await this.storage.store(transformedEntry);
result.imported++;
result.summary.newEntries++;
}
}
} catch (error) {
result.errors++;
result.summary.failedEntries++;
result.errorDetails.push(
error instanceof Error ? error.message : String(error),
);
}
}
// Import learning data if present
if (data.learning && !options.dryRun) {
await this.importLearningData(data.learning);
}
// Import knowledge graph if present
if (data.knowledgeGraph && !options.dryRun) {
await this.importKnowledgeGraphData(data.knowledgeGraph);
}
return result;
}
private mergeEntries(
existing: MemoryEntry,
imported: MemoryEntry,
): MemoryEntry {
return {
...existing,
...imported,
data: { ...existing.data, ...imported.data },
metadata: { ...existing.metadata, ...imported.metadata },
tags: [...new Set([...(existing.tags || []), ...(imported.tags || [])])],
timestamp: imported.timestamp || existing.timestamp,
};
}
private async importLearningData(learningData: any): Promise<void> {
if (learningData.patterns && Array.isArray(learningData.patterns)) {
for (const pattern of learningData.patterns) {
// This would require methods to import patterns into the learning system
// For now, just emit an event
this.emit("learning_pattern_imported", pattern);
}
}
}
private async importKnowledgeGraphData(kgData: any): Promise<void> {
if (kgData.nodes && Array.isArray(kgData.nodes)) {
for (const node of kgData.nodes) {
await this.knowledgeGraph.addNode(node);
}
}
if (kgData.edges && Array.isArray(kgData.edges)) {
for (const edge of kgData.edges) {
await this.knowledgeGraph.addEdge(edge);
}
}
}
private async createBackup(): Promise<string> {
const backupPath = `backup_${Date.now()}.json`;
const exportResult = await this.exportMemories(backupPath, {
format: "json",
includeMetadata: true,
includeLearning: true,
includeKnowledgeGraph: true,
});
this.emit("backup_created", { path: exportResult.filePath });
return exportResult.filePath || backupPath;
}
private convertToXML(data: any): string {
// Simple XML conversion - in production, use a proper XML library
const escapeXML = (str: string) =>
str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<export>\n';
xml += ` <metadata>\n`;
xml += ` <version>${escapeXML(data.metadata.version)}</version>\n`;
xml += ` <exportedAt>${escapeXML(
data.metadata.exportedAt,
)}</exportedAt>\n`;
xml += ` <entries>${data.metadata.entries}</entries>\n`;
xml += ` </metadata>\n`;
xml += ` <memories>\n`;
for (const memory of data.memories) {
xml += ` <memory>\n`;
xml += ` <id>${escapeXML(memory.id)}</id>\n`;
xml += ` <timestamp>${escapeXML(memory.timestamp)}</timestamp>\n`;
xml += ` <type>${escapeXML(memory.type)}</type>\n`;
xml += ` <data>${escapeXML(JSON.stringify(memory.data))}</data>\n`;
xml += ` </memory>\n`;
}
xml += ` </memories>\n`;
xml += "</export>";
return xml;
}
private convertToYAML(data: any): string {
// Simple YAML conversion - in production, use a proper YAML library
const indent = (level: number) => " ".repeat(level);
const toYAML = (obj: any, level: number = 0): string => {
if (obj === null) return "null";
if (typeof obj === "boolean") return obj.toString();
if (typeof obj === "number") return obj.toString();
if (typeof obj === "string") return `"${obj.replace(/"/g, '\\"')}"`;
if (Array.isArray(obj)) {
if (obj.length === 0) return "[]";
return (
"\n" +
obj
.map(
(item) => `${indent(level)}- ${toYAML(item, level + 1).trim()}`,
)
.join("\n")
);
}
if (typeof obj === "object") {
const keys = Object.keys(obj);
if (keys.length === 0) return "{}";
return (
"\n" +
keys
.map(
(key) =>
`${indent(level)}${key}: ${toYAML(obj[key], level + 1).trim()}`,
)
.join("\n")
);
}
return obj.toString();
};
return `# DocuMCP Memory Export\n${toYAML(data)}`;
}
// Additional helper methods for migration
private generateFieldMappings(
sourceSchema: any,
targetSchema: any,
): Record<string, string> {
const mappings: Record<string, string> = {};
// Simple field name matching - in production, use more sophisticated mapping
const sourceFields = Object.keys(sourceSchema.fields || {});
const targetFields = Object.keys(targetSchema.fields || {});
for (const sourceField of sourceFields) {
// Direct match
if (targetFields.includes(sourceField)) {
mappings[sourceField] = sourceField;
continue;
}
// Fuzzy matching
const similar = targetFields.find(
(tf) =>
tf.toLowerCase().includes(sourceField.toLowerCase()) ||
sourceField.toLowerCase().includes(tf.toLowerCase()),
);
if (similar) {
mappings[sourceField] = similar;
}
}
return mappings;
}
private generateTransformations(
sourceSchema: any,
targetSchema: any,
mapping: Record<string, string>,
): MigrationPlan["transformations"] {
const transformations: MigrationPlan["transformations"] = [];
// Generate transformations based on field mappings and type differences
for (const [sourceField, targetField] of Object.entries(mapping)) {
const sourceType = sourceSchema.fields?.[sourceField]?.type;
const targetType = targetSchema.fields?.[targetField]?.type;
if (sourceType !== targetType) {
transformations.push({
field: targetField,
type: "convert",
source: sourceField,
target: targetField,
operation: `${sourceType}_to_${targetType}`,
});
} else {
transformations.push({
field: targetField,
type: "rename",
source: sourceField,
target: targetField,
});
}
}
return transformations;
}
private generateValidationRules(
targetSchema: any,
): MigrationPlan["validation"] {
const validation: MigrationPlan["validation"] = [];
// Generate validation rules based on target schema
if (targetSchema.fields) {
for (const [field, config] of Object.entries(targetSchema.fields)) {
const rules: string[] = [];
const fieldConfig = config as any;
if (fieldConfig.required) {
rules.push("required");
}
if (fieldConfig.type) {
rules.push(`type:${fieldConfig.type}`);
}
if (fieldConfig.format) {
rules.push(`format:${fieldConfig.format}`);
}
validation.push({
field,
rules,
required: fieldConfig.required || false,
});
}
}
return validation;
}
private generatePostProcessingSteps(targetSchema: any): string[] {
const steps: string[] = [];
// Generate post-processing steps
steps.push("rebuild_indices");
steps.push("update_references");
steps.push("validate_integrity");
if (targetSchema.features?.learning) {
steps.push("retrain_models");
}
if (targetSchema.features?.knowledgeGraph) {
steps.push("rebuild_graph");
}
return steps;
}
private async loadRawData(inputPath: string): Promise<any> {
const content = await fs.readFile(inputPath, "utf8");
try {
return JSON.parse(content);
} catch {
return { raw: content };
}
}
private async applyTransformations(
data: any,
plan: MigrationPlan,
): Promise<any> {
const transformed = JSON.parse(JSON.stringify(data)); // Deep clone
for (const transformation of plan.transformations) {
// Apply transformation based on type
switch (transformation.type) {
case "rename":
this.renameField(
transformed,
transformation.source as string,
transformation.target,
);
break;
case "convert":
this.convertField(
transformed,
transformation.source as string,
transformation.target,
transformation.operation,
);
break;
// Add more transformation types as needed
}
}
return transformed;
}
private renameField(obj: any, oldName: string, newName: string): void {
if (typeof obj !== "object" || obj === null) return;
if (Array.isArray(obj)) {
obj.forEach((item) => this.renameField(item, oldName, newName));
} else {
if (oldName in obj) {
obj[newName] = obj[oldName];
delete obj[oldName];
}
Object.values(obj).forEach((value) =>
this.renameField(value, oldName, newName),
);
}
}
private convertField(
obj: any,
fieldName: string,
targetName: string,
operation?: string,
): void {
if (typeof obj !== "object" || obj === null) return;
if (Array.isArray(obj)) {
obj.forEach((item) =>
this.convertField(item, fieldName, targetName, operation),
);
} else {
if (fieldName in obj) {
const value = obj[fieldName];
// Apply conversion based on operation
switch (operation) {
case "string_to_number":
obj[targetName] = Number(value);
break;
case "number_to_string":
obj[targetName] = String(value);
break;
case "array_to_string":
obj[targetName] = Array.isArray(value) ? value.join(",") : value;
break;
case "string_to_array":
obj[targetName] =
typeof value === "string" ? value.split(",") : value;
break;
default:
obj[targetName] = value;
}
if (fieldName !== targetName) {
delete obj[fieldName];
}
}
Object.values(obj).forEach((value) =>
this.convertField(value, fieldName, targetName, operation),
);
}
}
private convertToImportFormat(data: any, plan: MigrationPlan): any {
// Convert transformed data to standard import format
const memories = Array.isArray(data) ? data : data.memories || [data];
// Convert old format to new MemoryEntry format
const convertedMemories = memories.map((entry: any) => {
// If already in new format, return as-is
if (entry.data && entry.metadata) {
return entry;
}
// Convert old flat format to new structured format
const converted: any = {
id:
entry.id ||
`migrated_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: entry.type || "analysis",
timestamp: entry.timestamp || new Date().toISOString(),
data: {},
metadata: {},
};
// Move known fields to appropriate locations
const dataFields = [
"language",
"recommendation",
"framework",
"outcome",
"success",
];
const metadataFields = [
"project",
"projectId",
"repository",
"ssg",
"tags",
];
for (const [key, value] of Object.entries(entry)) {
if (["id", "type", "timestamp"].includes(key)) {
// Already handled above
continue;
} else if (dataFields.includes(key)) {
converted.data[key] = value;
} else if (metadataFields.includes(key)) {
if (key === "project") {
converted.metadata.projectId = value; // Convert old 'project' field to 'projectId'
} else {
converted.metadata[key] = value;
}
} else {
// Put unknown fields in data
converted.data[key] = value;
}
}
return converted;
});
return {
metadata: {
version: this.version,
migrated: true,
migrationPlan: plan.sourceSystem,
importedAt: new Date().toISOString(),
},
memories: convertedMemories,
};
}
private async executePostProcessing(steps: string[]): Promise<void> {
for (const step of steps) {
try {
switch (step) {
case "rebuild_indices":
await this.storage.rebuildIndex();
break;
case "update_references":
// Update cross-references in data
break;
case "validate_integrity":
// Validate data integrity
break;
case "retrain_models":
// Trigger learning system retraining
break;
case "rebuild_graph":
// Rebuild knowledge graph
break;
}
this.emit("post_processing_step_completed", { step });
} catch (error) {
this.emit("post_processing_step_failed", {
step,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
private async loadSampleData(
sourcePath: string,
format: string,
): Promise<any> {
// Load a small sample of data for validation
if (format === "json") {
const content = await fs.readFile(sourcePath, "utf8");
const data = JSON.parse(content);
if (data.memories && Array.isArray(data.memories)) {
return { memories: data.memories.slice(0, 10) }; // First 10 entries
}
return data;
}
// For other formats, return basic structure info
return { format, sampleLoaded: true };
}
private validateSchema(sampleData: any): string[] {
const issues: string[] = [];
if (!sampleData.memories && !Array.isArray(sampleData)) {
issues.push("Expected memories array not found");
}
const memories =
sampleData.memories || (Array.isArray(sampleData) ? sampleData : []);
if (memories.length > 0) {
const firstEntry = memories[0];
if (!firstEntry.id) {
issues.push("Memory entries missing required id field");
}
if (!firstEntry.timestamp) {
issues.push("Memory entries missing required timestamp field");
}
if (!firstEntry.type) {
issues.push("Memory entries missing required type field");
}
if (!firstEntry.data) {
issues.push("Memory entries missing required data field");
}
}
return issues;
}
private validateDataIntegrity(sampleData: any): string[] {
const issues: string[] = [];
const memories =
sampleData.memories || (Array.isArray(sampleData) ? sampleData : []);
// Check for duplicate IDs
const ids = new Set();
const duplicates = new Set();
for (const entry of memories) {
if (entry.id) {
if (ids.has(entry.id)) {
duplicates.add(entry.id);
} else {
ids.add(entry.id);
}
}
}
if (duplicates.size > 0) {
issues.push(`Found ${duplicates.size} duplicate IDs`);
}
// Check timestamp validity
let invalidTimestamps = 0;
for (const entry of memories) {
if (entry.timestamp && isNaN(new Date(entry.timestamp).getTime())) {
invalidTimestamps++;
}
}
if (invalidTimestamps > 0) {
issues.push(`Found ${invalidTimestamps} invalid timestamps`);
}
return issues;
}
/**
* Apply field mappings and transformations to import data
*/
private applyDataTransformations(entry: any, options: ImportOptions): any {
const transformed = JSON.parse(JSON.stringify(entry)); // Deep clone
// Apply field mappings first
if (options.mapping) {
for (const [sourcePath, targetPath] of Object.entries(options.mapping)) {
const sourceValue = this.getValueByPath(transformed, sourcePath);
if (sourceValue !== undefined) {
this.setValueByPath(transformed, targetPath, sourceValue);
this.deleteValueByPath(transformed, sourcePath);
}
}
}
// Apply transformations
if (options.transformation?.enabled && options.transformation.rules) {
for (const rule of options.transformation.rules) {
switch (rule.operation) {
case "transform":
if (rule.params?.value !== undefined) {
this.setValueByPath(transformed, rule.field, rule.params.value);
}
break;
case "convert":
// Apply conversion based on params
break;
}
}
}
return transformed;
}
/**
* Get value from object using dot notation path
*/
private getValueByPath(obj: any, path: string): any {
return path.split(".").reduce((current, key) => current?.[key], obj);
}
/**
* Set value in object using dot notation path
*/
private setValueByPath(obj: any, path: string, value: any): void {
const keys = path.split(".");
const lastKey = keys.pop()!;
const target = keys.reduce((current, key) => {
if (!(key in current)) {
current[key] = {};
}
return current[key];
}, obj);
target[lastKey] = value;
}
/**
* Delete value from object using dot notation path
*/
private deleteValueByPath(obj: any, path: string): void {
const keys = path.split(".");
const lastKey = keys.pop()!;
const target = keys.reduce((current, key) => current?.[key], obj);
if (target && typeof target === "object") {
delete target[lastKey];
}
}
}
```
--------------------------------------------------------------------------------
/src/tools/validate-content.ts:
--------------------------------------------------------------------------------
```typescript
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { handleMemoryRecall } from "../memory/index.js";
import {
createExecutionSimulator,
ExecutionSimulator,
} from "../utils/execution-simulator.js";
// ESM-compatible dirname replacement - fallback for test environments
function getDirname(): string {
// Use process.cwd() as fallback for all environments to avoid import.meta issues
return process.cwd();
}
const currentDir = getDirname();
const execAsync = promisify(exec);
interface ValidationOptions {
contentPath: string;
analysisId?: string;
validationType: "accuracy" | "completeness" | "compliance" | "all";
includeCodeValidation: boolean;
confidence: "strict" | "moderate" | "permissive";
}
interface ConfidenceMetrics {
overall: number;
breakdown: {
technologyDetection: number;
frameworkVersionAccuracy: number;
codeExampleRelevance: number;
architecturalAssumptions: number;
businessContextAlignment: number;
};
riskFactors: RiskFactor[];
}
interface RiskFactor {
type: "high" | "medium" | "low";
category: string;
description: string;
impact: string;
mitigation: string;
}
interface UncertaintyFlag {
area: string;
severity: "low" | "medium" | "high" | "critical";
description: string;
potentialImpact: string;
clarificationNeeded: string;
fallbackStrategy: string;
}
interface ValidationIssue {
type: "error" | "warning" | "info";
category: "accuracy" | "completeness" | "compliance" | "performance";
location: {
file: string;
line?: number;
section?: string;
};
description: string;
evidence: string[];
suggestedFix: string;
confidence: number;
}
interface CodeValidationResult {
overallSuccess: boolean;
exampleResults: ExampleValidation[];
confidence: number;
}
interface ExampleValidation {
example: string;
compilationSuccess: boolean;
executionSuccess: boolean;
issues: ValidationIssue[];
confidence: number;
}
export interface ValidationResult {
success: boolean;
confidence: ConfidenceMetrics;
issues: ValidationIssue[];
uncertainties: UncertaintyFlag[];
codeValidation?: CodeValidationResult;
recommendations: string[];
nextSteps: string[];
}
class ContentAccuracyValidator {
private projectContext: any;
private tempDir: string;
private executionSimulator: ExecutionSimulator | null = null;
private useExecutionSimulation: boolean;
constructor(useExecutionSimulation: boolean = true) {
this.tempDir = path.join(currentDir, ".tmp");
this.useExecutionSimulation = useExecutionSimulation;
// Initialize execution simulator if enabled
if (useExecutionSimulation) {
this.executionSimulator = createExecutionSimulator({
maxDepth: 5,
maxSteps: 50,
timeoutMs: 15000,
detectNullRefs: true,
detectTypeMismatches: true,
detectUnreachableCode: true,
confidenceThreshold: 0.6,
});
}
}
async validateContent(
options: ValidationOptions,
context?: any,
): Promise<ValidationResult> {
if (context?.meta?.progressToken) {
await context.meta.reportProgress?.({ progress: 0, total: 100 });
}
const result: ValidationResult = {
success: false,
confidence: this.initializeConfidenceMetrics(),
issues: [],
uncertainties: [],
recommendations: [],
nextSteps: [],
};
// Load project context if analysis ID provided
if (options.analysisId) {
await context?.info?.("📊 Loading project context...");
this.projectContext = await this.loadProjectContext(options.analysisId);
}
if (context?.meta?.progressToken) {
await context.meta.reportProgress?.({ progress: 20, total: 100 });
}
// Determine if we should analyze application code vs documentation
await context?.info?.("🔎 Analyzing content type...");
const isApplicationValidation = await this.shouldAnalyzeApplicationCode(
options.contentPath,
);
if (context?.meta?.progressToken) {
await context.meta.reportProgress?.({ progress: 40, total: 100 });
}
// Perform different types of validation based on request
if (
options.validationType === "all" ||
options.validationType === "accuracy"
) {
await this.validateAccuracy(options.contentPath, result);
}
if (
options.validationType === "all" ||
options.validationType === "completeness"
) {
await this.validateCompleteness(options.contentPath, result);
}
if (
options.validationType === "all" ||
options.validationType === "compliance"
) {
if (isApplicationValidation) {
await this.validateApplicationStructureCompliance(
options.contentPath,
result,
);
} else {
await this.validateDiataxisCompliance(options.contentPath, result);
}
}
// Code validation if requested
if (options.includeCodeValidation) {
result.codeValidation = await this.validateCodeExamples(
options.contentPath,
);
// Set code example relevance confidence based on code validation results
if (result.codeValidation) {
const successRate =
result.codeValidation.exampleResults.length > 0
? result.codeValidation.exampleResults.filter(
(e) => e.compilationSuccess,
).length / result.codeValidation.exampleResults.length
: 1;
result.confidence.breakdown.codeExampleRelevance = Math.round(
successRate * 100,
);
}
} else {
// If code validation is skipped, assume reasonable confidence
result.confidence.breakdown.codeExampleRelevance = 75;
}
// Set framework version accuracy based on technology detection confidence
result.confidence.breakdown.frameworkVersionAccuracy = Math.min(
90,
result.confidence.breakdown.technologyDetection + 10,
);
// Set architectural assumptions confidence based on file structure and content analysis
const filesAnalyzed = await this.getMarkdownFiles(options.contentPath);
const hasStructuredContent = filesAnalyzed.length > 3; // Basic heuristic
result.confidence.breakdown.architecturalAssumptions = hasStructuredContent
? 80
: 60;
// Calculate overall confidence and success
this.calculateOverallMetrics(result);
// Generate recommendations and next steps
this.generateRecommendations(result, options);
if (context?.meta?.progressToken) {
await context.meta.reportProgress?.({ progress: 100, total: 100 });
}
const status = result.success ? "PASSED" : "ISSUES FOUND";
await context?.info?.(
`✅ Validation complete! Status: ${status} (${result.confidence.overall}% confidence, ${result.issues.length} issue(s))`,
);
return result;
}
private initializeConfidenceMetrics(): ConfidenceMetrics {
return {
overall: 0,
breakdown: {
technologyDetection: 0,
frameworkVersionAccuracy: 0,
codeExampleRelevance: 0,
architecturalAssumptions: 0,
businessContextAlignment: 0,
},
riskFactors: [],
};
}
private async loadProjectContext(analysisId: string): Promise<any> {
// Try to get analysis from memory system first
try {
const memoryRecall = await handleMemoryRecall({
query: analysisId,
type: "analysis",
limit: 1,
});
// Handle the memory recall result structure
if (
memoryRecall &&
memoryRecall.memories &&
memoryRecall.memories.length > 0
) {
const memory = memoryRecall.memories[0];
// Handle wrapped content structure
if (
memory.data &&
memory.data.content &&
Array.isArray(memory.data.content)
) {
// Extract the JSON from the first text content
const firstContent = memory.data.content[0];
if (
firstContent &&
firstContent.type === "text" &&
firstContent.text
) {
try {
return JSON.parse(firstContent.text);
} catch (parseError) {
console.warn(
"Failed to parse analysis content from memory:",
parseError,
);
}
}
}
// Try direct content or data access
if (memory.content) {
return memory.content;
}
if (memory.data) {
return memory.data;
}
}
} catch (error) {
console.warn("Failed to retrieve from memory system:", error);
}
// Fallback to reading from cached analysis file
try {
const analysisPath = path.join(
".documcp",
"analyses",
`${analysisId}.json`,
);
const content = await fs.readFile(analysisPath, "utf-8");
return JSON.parse(content);
} catch {
// Return default context if no analysis found
return {
metadata: { projectName: "unknown", primaryLanguage: "JavaScript" },
technologies: {},
dependencies: { packages: [] },
};
}
}
private async validateAccuracy(
contentPath: string,
result: ValidationResult,
): Promise<void> {
const files = await this.getMarkdownFiles(contentPath);
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
// Check for common accuracy issues
await this.checkTechnicalAccuracy(file, content, result);
await this.checkFrameworkVersionCompatibility(file, content, result);
await this.checkCommandAccuracy(file, content, result);
await this.checkLinkValidity(file, content, result);
}
// Update confidence based on findings
this.updateAccuracyConfidence(result);
}
private async checkTechnicalAccuracy(
filePath: string,
content: string,
result: ValidationResult,
): Promise<void> {
// Check for deprecated patterns
const deprecatedPatterns = [
{
pattern: /npm install -g/,
suggestion: "Use npx instead of global installs",
},
{ pattern: /var\s+\w+/, suggestion: "Use const or let instead of var" },
{ pattern: /function\(\)/, suggestion: "Consider using arrow functions" },
{ pattern: /http:\/\//, suggestion: "Use HTTPS URLs for security" },
];
for (const { pattern, suggestion } of deprecatedPatterns) {
if (pattern.test(content)) {
result.issues.push({
type: "warning",
category: "accuracy",
location: { file: path.basename(filePath) },
description: `Potentially outdated pattern detected: ${pattern.source}`,
evidence: [content.match(pattern)?.[0] || ""],
suggestedFix: suggestion,
confidence: 80,
});
}
}
// Check for missing error handling in code examples
const codeBlocks = this.extractCodeBlocks(content);
for (const block of codeBlocks) {
if (block.language === "javascript" || block.language === "typescript") {
if (
block.code.includes("await") &&
!block.code.includes("try") &&
!block.code.includes("catch")
) {
result.issues.push({
type: "warning",
category: "accuracy",
location: { file: path.basename(filePath) },
description: "Async code without error handling",
evidence: [block.code.substring(0, 100)],
suggestedFix: "Add try-catch blocks for async operations",
confidence: 90,
});
}
}
}
}
private async checkFrameworkVersionCompatibility(
filePath: string,
content: string,
result: ValidationResult,
): Promise<void> {
if (!this.projectContext) return;
// Check if mentioned versions align with project dependencies
const versionPattern = /@(\d+\.\d+\.\d+)/g;
const matches = content.match(versionPattern);
if (matches) {
for (const match of matches) {
const version = match.replace("@", "");
result.uncertainties.push({
area: "version-compatibility",
severity: "medium",
description: `Version ${version} mentioned in documentation`,
potentialImpact: "May not match actual project dependencies",
clarificationNeeded: "Verify version compatibility with project",
fallbackStrategy: "Use generic version-agnostic examples",
});
}
}
}
private async checkCommandAccuracy(
filePath: string,
content: string,
result: ValidationResult,
): Promise<void> {
const codeBlocks = this.extractCodeBlocks(content);
for (const block of codeBlocks) {
if (block.language === "bash" || block.language === "sh") {
// Check for common command issues
const commands = block.code
.split("\n")
.filter((line) => line.trim() && !line.startsWith("#"));
for (const command of commands) {
// Check for potentially dangerous commands
const dangerousPatterns = [
/rm -rf \//,
/sudo rm/,
/chmod 777/,
/> \/dev\/null 2>&1/,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(command)) {
result.issues.push({
type: "error",
category: "accuracy",
location: { file: path.basename(filePath) },
description: "Potentially dangerous command in documentation",
evidence: [command],
suggestedFix: "Review and provide safer alternative",
confidence: 95,
});
}
}
// Check for non-portable commands (Windows vs Unix)
if (command.includes("\\") && command.includes("/")) {
result.issues.push({
type: "warning",
category: "accuracy",
location: { file: path.basename(filePath) },
description: "Mixed path separators in command",
evidence: [command],
suggestedFix:
"Use consistent path separators or provide OS-specific examples",
confidence: 85,
});
}
}
}
}
}
private async checkLinkValidity(
filePath: string,
content: string,
result: ValidationResult,
): Promise<void> {
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
const links: Array<{ text: string; url: string }> = [];
let match;
while ((match = linkPattern.exec(content)) !== null) {
links.push({ text: match[1], url: match[2] });
}
for (const link of links) {
// Check internal links
if (!link.url.startsWith("http")) {
const targetPath = path.resolve(path.dirname(filePath), link.url);
try {
await fs.access(targetPath);
} catch {
result.issues.push({
type: "error",
category: "accuracy",
location: { file: path.basename(filePath) },
description: `Broken internal link: ${link.url}`,
evidence: [link.text],
suggestedFix: "Fix the link path or create the missing file",
confidence: 100,
});
}
}
// Flag external links for manual verification
if (link.url.startsWith("http")) {
result.uncertainties.push({
area: "external-links",
severity: "low",
description: `External link: ${link.url}`,
potentialImpact: "Link may become outdated or broken",
clarificationNeeded: "Verify link is still valid",
fallbackStrategy: "Archive important external content locally",
});
}
}
}
private async validateCompleteness(
contentPath: string,
result: ValidationResult,
): Promise<void> {
const files = await this.getMarkdownFiles(contentPath);
const structure = await this.analyzeDiataxisStructure(contentPath);
// Check for missing essential sections
const requiredSections = [
"tutorials",
"how-to",
"reference",
"explanation",
];
const missingSections = requiredSections.filter(
(section) => !structure.sections.includes(section),
);
if (missingSections.length > 0) {
result.issues.push({
type: "warning",
category: "completeness",
location: { file: "documentation structure" },
description: `Missing Diataxis sections: ${missingSections.join(", ")}`,
evidence: structure.sections,
suggestedFix:
"Add missing Diataxis sections for complete documentation",
confidence: 100,
});
}
// Check content depth in each section
for (const section of structure.sections) {
const sectionFiles = files.filter((f) => f.includes(`/${section}/`));
if (sectionFiles.length < 2) {
result.issues.push({
type: "info",
category: "completeness",
location: { file: section },
description: `Limited content in ${section} section`,
evidence: [`Only ${sectionFiles.length} files`],
suggestedFix: "Consider adding more comprehensive coverage",
confidence: 75,
});
}
}
// Update completeness confidence
result.confidence.breakdown.businessContextAlignment = Math.max(
0,
100 - missingSections.length * 25,
);
}
private async validateDiataxisCompliance(
contentPath: string,
result: ValidationResult,
): Promise<void> {
const files = await this.getMarkdownFiles(contentPath);
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const section = this.identifyDiataxisSection(file);
if (section) {
await this.checkSectionCompliance(file, content, section, result);
}
}
}
private async validateApplicationStructureCompliance(
contentPath: string,
result: ValidationResult,
): Promise<void> {
// Analyze application source code for Diataxis compliance
await this.validateSourceCodeDocumentation(contentPath, result);
await this.validateApplicationArchitecture(contentPath, result);
await this.validateInlineDocumentationPatterns(contentPath, result);
}
private async validateSourceCodeDocumentation(
contentPath: string,
result: ValidationResult,
): Promise<void> {
const sourceFiles = await this.getSourceFiles(contentPath);
for (const file of sourceFiles) {
const content = await fs.readFile(file, "utf-8");
// Check for proper JSDoc/TSDoc documentation
await this.checkInlineDocumentationQuality(file, content, result);
// Check for README files and their structure
if (file.endsWith("README.md")) {
await this.validateReadmeStructure(file, content, result);
}
// Check for proper module/class documentation
await this.checkModuleDocumentation(file, content, result);
}
}
private async validateApplicationArchitecture(
contentPath: string,
result: ValidationResult,
): Promise<void> {
// Check if the application structure supports different types of documentation
const hasToolsDir = await this.pathExists(path.join(contentPath, "tools"));
const hasTypesDir = await this.pathExists(path.join(contentPath, "types"));
// Check for workflows directory (currently not used but may be useful for future validation)
// const hasWorkflowsDir = await this.pathExists(path.join(contentPath, 'workflows'));
if (!hasToolsDir) {
result.issues.push({
type: "warning",
category: "compliance",
location: { file: "application structure" },
description:
"No dedicated tools directory found - may impact reference documentation organization",
evidence: ["Missing /tools directory"],
suggestedFix:
"Organize tools into dedicated directory for better reference documentation",
confidence: 80,
});
}
if (!hasTypesDir) {
result.issues.push({
type: "info",
category: "compliance",
location: { file: "application structure" },
description:
"No types directory found - may impact API reference documentation",
evidence: ["Missing /types directory"],
suggestedFix: "Consider organizing types for better API documentation",
confidence: 70,
});
}
}
private async validateInlineDocumentationPatterns(
contentPath: string,
result: ValidationResult,
): Promise<void> {
const sourceFiles = await this.getSourceFiles(contentPath);
for (const file of sourceFiles) {
const content = await fs.readFile(file, "utf-8");
// Check for proper function documentation that could support tutorials
const functions = this.extractFunctions(content);
for (const func of functions) {
if (func.isExported && !func.hasDocumentation) {
result.issues.push({
type: "warning",
category: "compliance",
location: { file: path.basename(file), line: func.line },
description: `Exported function '${func.name}' lacks documentation`,
evidence: [func.signature],
suggestedFix:
"Add JSDoc/TSDoc documentation to support tutorial and reference content",
confidence: 85,
});
}
}
// Check for proper error handling documentation
const errorPatterns = content.match(/throw new \w*Error/g);
if (errorPatterns && errorPatterns.length > 0) {
const hasErrorDocs =
content.includes("@throws") || content.includes("@error");
if (!hasErrorDocs) {
result.issues.push({
type: "info",
category: "compliance",
location: { file: path.basename(file) },
description:
"Error throwing code found without error documentation",
evidence: errorPatterns,
suggestedFix:
"Document error conditions to support troubleshooting guides",
confidence: 75,
});
}
}
}
}
private identifyDiataxisSection(filePath: string): string | null {
const sections = ["tutorials", "how-to", "reference", "explanation"];
for (const section of sections) {
if (filePath.includes(`/${section}/`)) {
return section;
}
}
return null;
}
private async checkSectionCompliance(
filePath: string,
content: string,
section: string,
result: ValidationResult,
): Promise<void> {
const complianceRules = this.getDiataxisComplianceRules(section);
for (const rule of complianceRules) {
if (!rule.check(content)) {
result.issues.push({
type: "warning",
category: "compliance",
location: { file: path.basename(filePath), section },
description: rule.message,
evidence: [rule.evidence?.(content) || ""],
suggestedFix: rule.fix,
confidence: rule.confidence,
});
}
}
}
private getDiataxisComplianceRules(section: string) {
const rules: any = {
tutorials: [
{
check: (content: string) =>
content.includes("## Prerequisites") ||
content.includes("## Requirements"),
message: "Tutorial should include prerequisites section",
fix: "Add a prerequisites or requirements section",
confidence: 90,
},
{
check: (content: string) => /step|Step|STEP/.test(content),
message: "Tutorial should be organized in clear steps",
fix: "Structure content with numbered steps or clear progression",
confidence: 85,
},
{
check: (content: string) => content.includes("```"),
message: "Tutorial should include practical code examples",
fix: "Add code blocks with working examples",
confidence: 80,
},
],
"how-to": [
{
check: (content: string) => /how to|How to|HOW TO/.test(content),
message: "How-to guide should focus on specific tasks",
fix: "Frame content around achieving specific goals",
confidence: 75,
},
{
check: (content: string) => content.length > 500,
message: "How-to guide should provide detailed guidance",
fix: "Expand with more detailed instructions",
confidence: 70,
},
],
reference: [
{
check: (content: string) => /##|###/.test(content),
message: "Reference should be well-structured with clear sections",
fix: "Add proper headings and organization",
confidence: 95,
},
{
check: (content: string) => /\|.*\|/.test(content),
message: "Reference should include tables for structured information",
fix: "Consider using tables for parameters, options, etc.",
confidence: 60,
},
],
explanation: [
{
check: (content: string) =>
content.includes("why") || content.includes("Why"),
message: 'Explanation should address the "why" behind concepts',
fix: "Include rationale and context for decisions/concepts",
confidence: 80,
},
{
check: (content: string) => content.length > 800,
message: "Explanation should provide in-depth coverage",
fix: "Expand with more comprehensive explanation",
confidence: 70,
},
],
};
return rules[section] || [];
}
private async validateCodeExamples(
contentPath: string,
): Promise<CodeValidationResult> {
const files = await this.getMarkdownFiles(contentPath);
const allExamples: ExampleValidation[] = [];
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const codeBlocks = this.extractCodeBlocks(content);
for (const block of codeBlocks) {
if (this.isValidatableLanguage(block.language)) {
const validation = await this.validateCodeBlock(block, file);
allExamples.push(validation);
}
}
}
return {
overallSuccess: allExamples.every((e) => e.compilationSuccess),
exampleResults: allExamples,
confidence: this.calculateCodeValidationConfidence(allExamples),
};
}
private extractCodeBlocks(
content: string,
): Array<{ language: string; code: string; id: string }> {
const codeBlockPattern = /```(\w+)?\n([\s\S]*?)```/g;
const blocks: Array<{ language: string; code: string; id: string }> = [];
let match;
let index = 0;
while ((match = codeBlockPattern.exec(content)) !== null) {
blocks.push({
language: match[1] || "text",
code: match[2].trim(),
id: `block-${index++}`,
});
}
return blocks;
}
private isValidatableLanguage(language: string): boolean {
const validatable = [
"javascript",
"typescript",
"js",
"ts",
"json",
"bash",
"sh",
];
return validatable.includes(language.toLowerCase());
}
private async validateCodeBlock(
block: { language: string; code: string; id: string },
filePath: string,
): Promise<ExampleValidation> {
const validation: ExampleValidation = {
example: block.id,
compilationSuccess: false,
executionSuccess: false,
issues: [],
confidence: 0,
};
try {
if (block.language === "typescript" || block.language === "ts") {
await this.validateTypeScriptCode(block.code, validation);
// Use execution simulation for additional validation if available
if (
this.useExecutionSimulation &&
this.executionSimulator &&
validation.compilationSuccess
) {
await this.enhanceWithExecutionSimulation(
block.code,
validation,
filePath,
);
}
} else if (block.language === "javascript" || block.language === "js") {
await this.validateJavaScriptCode(block.code, validation);
// Use execution simulation for additional validation if available
if (
this.useExecutionSimulation &&
this.executionSimulator &&
validation.compilationSuccess
) {
await this.enhanceWithExecutionSimulation(
block.code,
validation,
filePath,
);
}
} else if (block.language === "json") {
await this.validateJSONCode(block.code, validation);
} else if (block.language === "bash" || block.language === "sh") {
await this.validateBashCode(block.code, validation);
}
} catch (error: any) {
validation.issues.push({
type: "error",
category: "accuracy",
location: { file: path.basename(filePath) },
description: `Code validation failed: ${error.message}`,
evidence: [block.code.substring(0, 100)],
suggestedFix: "Review and fix syntax errors",
confidence: 95,
});
}
return validation;
}
/**
* Enhance validation with execution simulation results
*/
private async enhanceWithExecutionSimulation(
code: string,
validation: ExampleValidation,
filePath: string,
): Promise<void> {
if (!this.executionSimulator) return;
try {
const simResult = await this.executionSimulator.validateExample(
code,
code,
);
// Add simulation-detected issues to validation
for (const issue of simResult.issues) {
validation.issues.push({
type: issue.severity === "error" ? "error" : "warning",
category: "accuracy",
location: {
file: path.basename(filePath),
line: issue.location.line,
},
description: `[Simulation] ${issue.description}`,
evidence: [issue.codeSnippet || ""],
suggestedFix: issue.suggestion,
confidence: Math.round(simResult.trace.confidenceScore * 100),
});
}
// Update execution success based on simulation
validation.executionSuccess = simResult.isValid;
// Adjust confidence based on simulation confidence
if (simResult.trace.confidenceScore > 0) {
validation.confidence = Math.round(
(validation.confidence + simResult.trace.confidenceScore * 100) / 2,
);
}
} catch (error) {
// Simulation is best-effort, don't fail validation on simulation errors
console.warn("Execution simulation failed:", error);
}
}
private async validateTypeScriptCode(
code: string,
validation: ExampleValidation,
): Promise<void> {
// Ensure temp directory exists
await fs.mkdir(this.tempDir, { recursive: true });
const tempFile = path.join(this.tempDir, `temp-${Date.now()}.ts`);
try {
// Write code to temporary file
await fs.writeFile(tempFile, code, "utf-8");
// Try to compile with TypeScript
const { stderr } = await execAsync(
`npx tsc --noEmit --skipLibCheck ${tempFile}`,
);
if (stderr && stderr.includes("error")) {
validation.issues.push({
type: "error",
category: "accuracy",
location: { file: "code-example" },
description: "TypeScript compilation error",
evidence: [stderr],
suggestedFix: "Fix TypeScript syntax and type errors",
confidence: 90,
});
} else {
validation.compilationSuccess = true;
validation.confidence = 85;
}
} catch (error: any) {
if (error.stderr && error.stderr.includes("error")) {
validation.issues.push({
type: "error",
category: "accuracy",
location: { file: "code-example" },
description: "TypeScript compilation failed",
evidence: [error.stderr],
suggestedFix: "Fix compilation errors",
confidence: 95,
});
}
} finally {
// Clean up temp file
try {
await fs.unlink(tempFile);
} catch {
// Ignore cleanup errors
}
}
}
private async validateJavaScriptCode(
code: string,
validation: ExampleValidation,
): Promise<void> {
try {
// Basic syntax check using Node.js
new Function(code);
validation.compilationSuccess = true;
validation.confidence = 75;
} catch (error: any) {
validation.issues.push({
type: "error",
category: "accuracy",
location: { file: "code-example" },
description: `JavaScript syntax error: ${error.message}`,
evidence: [code.substring(0, 100)],
suggestedFix: "Fix JavaScript syntax errors",
confidence: 90,
});
}
}
private async validateJSONCode(
code: string,
validation: ExampleValidation,
): Promise<void> {
try {
JSON.parse(code);
validation.compilationSuccess = true;
validation.confidence = 95;
} catch (error: any) {
validation.issues.push({
type: "error",
category: "accuracy",
location: { file: "code-example" },
description: `Invalid JSON: ${error.message}`,
evidence: [code.substring(0, 100)],
suggestedFix: "Fix JSON syntax errors",
confidence: 100,
});
}
}
private async validateBashCode(
code: string,
validation: ExampleValidation,
): Promise<void> {
// Basic bash syntax validation
const lines = code
.split("\n")
.filter((line) => line.trim() && !line.startsWith("#"));
for (const line of lines) {
// Check for basic syntax issues
if (line.includes("&&") && line.includes("||")) {
validation.issues.push({
type: "warning",
category: "accuracy",
location: { file: "code-example" },
description: "Complex command chaining may be confusing",
evidence: [line],
suggestedFix:
"Consider breaking into separate commands or adding explanation",
confidence: 60,
});
}
// Check for unquoted variables in dangerous contexts
if (line.includes("rm") && /\$\w+/.test(line) && !/'.*\$.*'/.test(line)) {
validation.issues.push({
type: "warning",
category: "accuracy",
location: { file: "code-example" },
description: "Unquoted variable in potentially dangerous command",
evidence: [line],
suggestedFix: "Quote variables to prevent word splitting",
confidence: 80,
});
}
}
validation.compilationSuccess =
validation.issues.filter((i) => i.type === "error").length === 0;
validation.confidence = validation.compilationSuccess ? 70 : 20;
}
private calculateCodeValidationConfidence(
examples: ExampleValidation[],
): number {
if (examples.length === 0) return 0;
const totalConfidence = examples.reduce(
(sum, ex) => sum + ex.confidence,
0,
);
return Math.round(totalConfidence / examples.length);
}
public async getMarkdownFiles(
contentPath: string,
maxDepth: number = 5,
): Promise<string[]> {
const files: string[] = [];
const excludedDirs = new Set([
"node_modules",
".git",
"dist",
"build",
".next",
".nuxt",
"coverage",
".tmp",
"tmp",
".cache",
".vscode",
".idea",
"logs",
".logs",
".npm",
".yarn",
]);
const scan = async (
dir: string,
currentDepth: number = 0,
): Promise<void> => {
if (currentDepth > maxDepth) return;
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip excluded directories
if (excludedDirs.has(entry.name) || entry.name.startsWith(".")) {
continue;
}
// Prevent symlink loops
try {
const stats = await fs.lstat(fullPath);
if (stats.isSymbolicLink()) {
continue;
}
} catch {
continue;
}
await scan(fullPath, currentDepth + 1);
} else if (entry.name.endsWith(".md")) {
files.push(fullPath);
// Limit total files to prevent memory issues
if (files.length > 500) {
console.warn("Markdown file limit reached (500), stopping scan");
return;
}
}
}
} catch (error) {
// Skip directories that can't be read
console.warn(`Warning: Could not read directory ${dir}:`, error);
}
};
try {
await scan(contentPath);
} catch (error) {
console.warn("Error scanning directory:", error);
}
return files;
}
private async getSourceFiles(
contentPath: string,
maxDepth: number = 5,
): Promise<string[]> {
const files: string[] = [];
const excludedDirs = new Set([
"node_modules",
".git",
"dist",
"build",
".next",
".nuxt",
"coverage",
".tmp",
"tmp",
".cache",
".vscode",
".idea",
"logs",
".logs",
".npm",
".yarn",
]);
const scan = async (
dir: string,
currentDepth: number = 0,
): Promise<void> => {
if (currentDepth > maxDepth) return;
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip excluded directories
if (excludedDirs.has(entry.name) || entry.name.startsWith(".")) {
continue;
}
// Prevent symlink loops
try {
const stats = await fs.lstat(fullPath);
if (stats.isSymbolicLink()) {
continue;
}
} catch {
continue;
}
await scan(fullPath, currentDepth + 1);
} else if (
entry.name.endsWith(".ts") ||
entry.name.endsWith(".js") ||
entry.name.endsWith(".md")
) {
files.push(fullPath);
// Limit total files to prevent memory issues
if (files.length > 1000) {
console.warn("File limit reached (1000), stopping scan");
return;
}
}
}
} catch (error) {
// Skip directories that can't be read
console.warn(`Warning: Could not read directory ${dir}:`, error);
}
};
try {
await scan(contentPath);
} catch (error) {
console.warn("Error scanning directory:", error);
}
return files;
}
private async pathExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
private extractFunctions(content: string): Array<{
name: string;
line: number;
signature: string;
isExported: boolean;
hasDocumentation: boolean;
}> {
const functions: Array<{
name: string;
line: number;
signature: string;
isExported: boolean;
hasDocumentation: boolean;
}> = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match function declarations and exports
const functionMatch = line.match(
/^(export\s+)?(async\s+)?function\s+(\w+)/,
);
const arrowMatch = line.match(
/^(export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(/,
);
if (functionMatch) {
const name = functionMatch[3];
const isExported = !!functionMatch[1];
const hasDocumentation = this.checkForDocumentation(lines, i);
functions.push({
name,
line: i + 1,
signature: line.trim(),
isExported,
hasDocumentation,
});
} else if (arrowMatch) {
const name = arrowMatch[2];
const isExported = !!arrowMatch[1];
const hasDocumentation = this.checkForDocumentation(lines, i);
functions.push({
name,
line: i + 1,
signature: line.trim(),
isExported,
hasDocumentation,
});
}
}
return functions;
}
private async checkInlineDocumentationQuality(
_file: string,
_content: string,
_result: ValidationResult,
): Promise<void> {
// Implementation for checking JSDoc/TSDoc quality
// This could check for proper parameter documentation, return types, etc.
}
private async validateReadmeStructure(
_file: string,
content: string,
result: ValidationResult,
): Promise<void> {
// Check if README follows good structure
const hasTitle = /^#\s+/.test(content);
const hasDescription =
content.includes("## Description") || content.includes("## Overview");
const hasInstallation =
content.includes("## Installation") || content.includes("## Setup");
const hasUsage =
content.includes("## Usage") || content.includes("## Getting Started");
if (!hasTitle) {
result.issues.push({
type: "warning",
category: "compliance",
location: { file: "README.md" },
description: "README missing clear title",
evidence: ["No H1 heading found"],
suggestedFix: "Add clear title with # heading",
confidence: 90,
});
}
if (!hasDescription && !hasInstallation && !hasUsage) {
result.issues.push({
type: "warning",
category: "compliance",
location: { file: "README.md" },
description:
"README lacks essential sections (description, installation, usage)",
evidence: ["Missing standard README sections"],
suggestedFix:
"Add sections for description, installation, and usage following Diataxis principles",
confidence: 85,
});
}
}
private async checkModuleDocumentation(
_file: string,
_content: string,
_result: ValidationResult,
): Promise<void> {
// Implementation for checking module-level documentation
// This could check for file-level JSDoc, proper exports documentation, etc.
}
private checkForDocumentation(
lines: string[],
functionLineIndex: number,
): boolean {
// Look backwards from the function line to find documentation
let checkIndex = functionLineIndex - 1;
// Skip empty lines
while (checkIndex >= 0 && lines[checkIndex].trim() === "") {
checkIndex--;
}
// Check if we found the end of a JSDoc comment
if (checkIndex >= 0 && lines[checkIndex].trim() === "*/") {
// Look backwards to find the start of the JSDoc block
let jsDocStart = checkIndex;
while (jsDocStart >= 0) {
const line = lines[jsDocStart].trim();
if (line.startsWith("/**")) {
return true; // Found complete JSDoc block
}
if (!line.startsWith("*") && line !== "*/") {
break; // Not part of JSDoc block
}
jsDocStart--;
}
}
// Also check for single-line JSDoc comments
if (
checkIndex >= 0 &&
lines[checkIndex].trim().startsWith("/**") &&
lines[checkIndex].includes("*/")
) {
return true;
}
return false;
}
private async shouldAnalyzeApplicationCode(
contentPath: string,
): Promise<boolean> {
// Check if the path contains application source code vs documentation
const hasSrcDir = await this.pathExists(path.join(contentPath, "src"));
const hasPackageJson = await this.pathExists(
path.join(contentPath, "package.json"),
);
const hasTypescriptFiles = (await this.getSourceFiles(contentPath)).some(
(file) => file.endsWith(".ts"),
);
// If path ends with 'src' or is a project root with src/, analyze as application code
if (
contentPath.endsWith("/src") ||
contentPath.endsWith("\\src") ||
(hasSrcDir && hasPackageJson)
) {
return true;
}
// If path contains TypeScript/JavaScript files and package.json, treat as application code
if (hasTypescriptFiles && hasPackageJson) {
return true;
}
// If path is specifically a documentation directory, analyze as documentation
if (contentPath.includes("/docs") || contentPath.includes("\\docs")) {
return false;
}
return false;
}
private async analyzeDiataxisStructure(
contentPath: string,
): Promise<{ sections: string[] }> {
const sections: string[] = [];
try {
const entries = await fs.readdir(contentPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const dirName = entry.name;
if (
["tutorials", "how-to", "reference", "explanation"].includes(
dirName,
)
) {
sections.push(dirName);
}
}
}
} catch {
// Directory doesn't exist
}
return { sections };
}
private updateAccuracyConfidence(result: ValidationResult): void {
const errorCount = result.issues.filter((i) => i.type === "error").length;
const warningCount = result.issues.filter(
(i) => i.type === "warning",
).length;
// Base confidence starts high and decreases with issues
let confidence = 95;
confidence -= errorCount * 20;
confidence -= warningCount * 5;
confidence = Math.max(0, confidence);
result.confidence.breakdown.technologyDetection = confidence;
}
private calculateOverallMetrics(result: ValidationResult): void {
const breakdown = result.confidence.breakdown;
const values = Object.values(breakdown).filter((v) => v > 0);
if (values.length > 0) {
result.confidence.overall = Math.round(
values.reduce((a, b) => a + b, 0) / values.length,
);
}
// Determine overall success
const criticalIssues = result.issues.filter(
(i) => i.type === "error",
).length;
result.success = criticalIssues === 0;
// Add risk factors based on issues
if (criticalIssues > 0) {
result.confidence.riskFactors.push({
type: "high",
category: "accuracy",
description: `${criticalIssues} critical accuracy issues found`,
impact: "Users may encounter broken examples or incorrect information",
mitigation: "Fix all critical issues before publication",
});
}
const uncertaintyCount = result.uncertainties.length;
if (uncertaintyCount > 5) {
result.confidence.riskFactors.push({
type: "medium",
category: "completeness",
description: `${uncertaintyCount} areas requiring clarification`,
impact: "Documentation may lack specificity for user context",
mitigation: "Address high-priority uncertainties with user input",
});
}
}
private generateRecommendations(
result: ValidationResult,
_options: ValidationOptions,
): void {
const recommendations: string[] = [];
const nextSteps: string[] = [];
// Generate recommendations based on issues found
const errorCount = result.issues.filter((i) => i.type === "error").length;
if (errorCount > 0) {
recommendations.push(
`Fix ${errorCount} critical accuracy issues before publication`,
);
nextSteps.push("Review and resolve all error-level validation issues");
}
const warningCount = result.issues.filter(
(i) => i.type === "warning",
).length;
if (warningCount > 0) {
recommendations.push(
`Address ${warningCount} potential accuracy concerns`,
);
nextSteps.push(
"Review warning-level issues and apply fixes where appropriate",
);
}
const uncertaintyCount = result.uncertainties.filter(
(u) => u.severity === "high" || u.severity === "critical",
).length;
if (uncertaintyCount > 0) {
recommendations.push(
`Clarify ${uncertaintyCount} high-uncertainty areas`,
);
nextSteps.push("Gather user input on areas flagged for clarification");
}
// Code validation recommendations
if (result.codeValidation && !result.codeValidation.overallSuccess) {
recommendations.push(
"Fix code examples that fail compilation or execution tests",
);
nextSteps.push(
"Test all code examples in appropriate development environment",
);
}
// Completeness recommendations
const missingCompliance = result.issues.filter(
(i) => i.category === "compliance",
).length;
if (missingCompliance > 0) {
recommendations.push(
"Improve Diataxis framework compliance for better user experience",
);
nextSteps.push(
"Restructure content to better align with Diataxis principles",
);
}
// General recommendations based on confidence level
if (result.confidence.overall < 70) {
recommendations.push(
"Overall confidence is below recommended threshold - consider comprehensive review",
);
nextSteps.push(
"Conduct manual review of generated content before publication",
);
}
if (recommendations.length === 0) {
recommendations.push("Content validation passed - ready for publication");
nextSteps.push("Deploy documentation and monitor for user feedback");
}
result.recommendations = recommendations;
result.nextSteps = nextSteps;
}
}
export const validateDiataxisContent: Tool = {
name: "validate_diataxis_content",
description:
"Validate the accuracy, completeness, and compliance of generated Diataxis documentation",
inputSchema: {
type: "object",
properties: {
contentPath: {
type: "string",
description: "Path to the documentation directory to validate",
},
analysisId: {
type: "string",
description:
"Optional repository analysis ID for context-aware validation",
},
validationType: {
type: "string",
enum: ["accuracy", "completeness", "compliance", "all"],
default: "all",
description: "Type of validation to perform",
},
includeCodeValidation: {
type: "boolean",
default: true,
description: "Whether to validate code examples for correctness",
},
confidence: {
type: "string",
enum: ["strict", "moderate", "permissive"],
default: "moderate",
description:
"Validation confidence level - stricter levels catch more issues",
},
},
required: ["contentPath"],
},
};
/**
* Validates Diataxis-compliant documentation content for accuracy, completeness, and compliance.
*
* Performs comprehensive validation of documentation content including accuracy verification,
* completeness assessment, compliance checking, and code example validation. Uses advanced
* confidence scoring and risk assessment to provide detailed validation results with
* actionable recommendations.
*
* @param args - The validation parameters
* @param args.contentPath - Path to the documentation content directory
* @param args.analysisId - Optional repository analysis ID for context-aware validation
* @param args.validationType - Type of validation to perform: "accuracy", "completeness", "compliance", or "all"
* @param args.includeCodeValidation - Whether to validate code examples and syntax
* @param args.confidence - Validation confidence level: "strict", "moderate", or "permissive"
*
* @returns Promise resolving to comprehensive validation results
* @returns success - Whether validation passed overall
* @returns confidence - Confidence metrics and risk assessment
* @returns issues - Array of validation issues found
* @returns uncertainties - Areas requiring clarification
* @returns codeValidation - Code example validation results
* @returns recommendations - Suggested improvements
* @returns nextSteps - Recommended next actions
*
* @throws {Error} When content path is inaccessible
* @throws {Error} When validation processing fails
*
* @example
* ```typescript
* // Comprehensive validation
* const result = await handleValidateDiataxisContent({
* contentPath: "./docs",
* validationType: "all",
* includeCodeValidation: true,
* confidence: "moderate"
* });
*
* console.log(`Validation success: ${result.success}`);
* console.log(`Overall confidence: ${result.confidence.overall}%`);
* console.log(`Issues found: ${result.issues.length}`);
*
* // Strict accuracy validation
* const accuracy = await handleValidateDiataxisContent({
* contentPath: "./docs",
* validationType: "accuracy",
* confidence: "strict"
* });
* ```
*
* @since 1.0.0
*/
export async function handleValidateDiataxisContent(
args: any,
context?: any,
): Promise<ValidationResult> {
await context?.info?.("🔍 Starting Diataxis content validation...");
const validator = new ContentAccuracyValidator();
// Add timeout protection to prevent infinite hangs
const timeoutMs = 120000; // 2 minutes
let timeoutHandle: NodeJS.Timeout;
const timeoutPromise = new Promise<ValidationResult>((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(
new Error(
`Validation timed out after ${
timeoutMs / 1000
} seconds. This may be due to a large directory structure. Try validating a smaller subset or specific directory.`,
),
);
}, timeoutMs);
});
const validationPromise = validator.validateContent(args, context);
try {
const result = await Promise.race([validationPromise, timeoutPromise]);
clearTimeout(timeoutHandle!);
return result;
} catch (error: any) {
clearTimeout(timeoutHandle!);
// Return a partial result with error information
return {
success: false,
confidence: {
overall: 0,
breakdown: {
technologyDetection: 0,
frameworkVersionAccuracy: 0,
codeExampleRelevance: 0,
architecturalAssumptions: 0,
businessContextAlignment: 0,
},
riskFactors: [
{
type: "high",
category: "validation",
description: "Validation process failed or timed out",
impact: "Unable to complete content validation",
mitigation:
"Try validating a smaller directory or specific subset of files",
},
],
},
issues: [],
uncertainties: [],
recommendations: [
"Validation failed or timed out",
"Consider validating smaller directory subsets",
"Check for very large files or deep directory structures",
`Error: ${error.message}`,
],
nextSteps: [
"Verify the content path is correct and accessible",
"Try validating specific subdirectories instead of the entire project",
"Check for circular symlinks or very deep directory structures",
],
};
}
}
interface GeneralValidationResult {
success: boolean;
linksChecked: number;
brokenLinks: string[];
codeBlocksValidated: number;
codeErrors: string[];
recommendations: string[];
summary: string;
}
export async function validateGeneralContent(
args: any,
): Promise<GeneralValidationResult> {
const {
contentPath,
validationType = "all",
includeCodeValidation = true,
followExternalLinks = false,
} = args;
const result: GeneralValidationResult = {
success: true,
linksChecked: 0,
brokenLinks: [],
codeBlocksValidated: 0,
codeErrors: [],
recommendations: [],
summary: "",
};
try {
// Get all markdown files
const validator = new ContentAccuracyValidator();
const files = await validator.getMarkdownFiles(contentPath);
// Check links if requested
if (validationType === "all" || validationType === "links") {
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const links = extractLinksFromMarkdown(content);
for (const link of links) {
result.linksChecked++;
// Skip external links unless explicitly requested
if (link.startsWith("http") && !followExternalLinks) continue;
// Check internal links
if (!link.startsWith("http")) {
const fullPath = path.resolve(path.dirname(file), link);
try {
await fs.access(fullPath);
} catch {
result.brokenLinks.push(`${path.basename(file)}: ${link}`);
result.success = false;
}
}
}
}
}
// Validate code blocks if requested
if (
includeCodeValidation &&
(validationType === "all" || validationType === "code")
) {
for (const file of files) {
const content = await fs.readFile(file, "utf-8");
const codeBlocks = extractCodeBlocks(content);
for (const block of codeBlocks) {
result.codeBlocksValidated++;
// Basic syntax validation
if (block.language && block.code.trim()) {
if (block.language === "javascript" || block.language === "js") {
try {
// Basic JS syntax check - look for common issues
if (
block.code.includes("console.log") &&
!block.code.includes(";")
) {
result.codeErrors.push(
`${path.basename(file)}: Missing semicolon in JS code`,
);
}
} catch (error) {
result.codeErrors.push(
`${path.basename(file)}: JS syntax error - ${error}`,
);
result.success = false;
}
}
}
}
}
}
// Generate recommendations
if (result.brokenLinks.length > 0) {
result.recommendations.push(
`Fix ${result.brokenLinks.length} broken internal links`,
);
result.recommendations.push(
"Run documentation build to catch additional link issues",
);
}
if (result.codeErrors.length > 0) {
result.recommendations.push(
`Review and fix ${result.codeErrors.length} code syntax issues`,
);
}
if (result.success) {
result.recommendations.push(
"Content validation passed - no critical issues found",
);
}
// Create summary
result.summary = `Validated ${files.length} files, ${
result.linksChecked
} links, ${result.codeBlocksValidated} code blocks. ${
result.success
? "PASSED"
: `ISSUES FOUND: ${
result.brokenLinks.length + result.codeErrors.length
}`
}`;
return result;
} catch (error) {
result.success = false;
result.recommendations.push(`Validation failed: ${error}`);
result.summary = `Validation error: ${error}`;
return result;
}
}
// Helper function to extract links from markdown
function extractLinksFromMarkdown(content: string): string[] {
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
const links: string[] = [];
let match;
while ((match = linkRegex.exec(content)) !== null) {
links.push(match[2]); // The URL part
}
return links;
}
// Helper function to extract code blocks from markdown
function extractCodeBlocks(
content: string,
): { language: string; code: string }[] {
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
const blocks: { language: string; code: string }[] = [];
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
blocks.push({
language: match[1] || "text",
code: match[2],
});
}
return blocks;
}
```
--------------------------------------------------------------------------------
/src/utils/ast-analyzer.ts:
--------------------------------------------------------------------------------
```typescript
/**
* AST-based Code Analyzer (Phase 3)
*
* Uses tree-sitter parsers for multi-language AST analysis
* Provides deep code structure extraction for drift detection
*/
import { parse as parseTypeScript } from "@typescript-eslint/typescript-estree";
import { promises as fs } from "fs";
import path from "path";
import crypto from "crypto";
// Language configuration
const LANGUAGE_CONFIGS: Record<
string,
{ parser: string; extensions: string[] }
> = {
typescript: { parser: "tree-sitter-typescript", extensions: [".ts", ".tsx"] },
javascript: {
parser: "tree-sitter-javascript",
extensions: [".js", ".jsx", ".mjs"],
},
python: { parser: "tree-sitter-python", extensions: [".py"] },
rust: { parser: "tree-sitter-rust", extensions: [".rs"] },
go: { parser: "tree-sitter-go", extensions: [".go"] },
java: { parser: "tree-sitter-java", extensions: [".java"] },
ruby: { parser: "tree-sitter-ruby", extensions: [".rb"] },
bash: { parser: "tree-sitter-bash", extensions: [".sh", ".bash"] },
};
export interface FunctionSignature {
name: string;
parameters: ParameterInfo[];
returnType: string | null;
isAsync: boolean;
isExported: boolean;
isPublic: boolean;
docComment: string | null;
startLine: number;
endLine: number;
complexity: number;
dependencies: string[];
}
export interface ParameterInfo {
name: string;
type: string | null;
optional: boolean;
defaultValue: string | null;
}
export interface ClassInfo {
name: string;
isExported: boolean;
extends: string | null;
implements: string[];
methods: FunctionSignature[];
properties: PropertyInfo[];
docComment: string | null;
startLine: number;
endLine: number;
}
export interface PropertyInfo {
name: string;
type: string | null;
isStatic: boolean;
isReadonly: boolean;
visibility: "public" | "private" | "protected";
}
export interface InterfaceInfo {
name: string;
isExported: boolean;
extends: string[];
properties: PropertyInfo[];
methods: FunctionSignature[];
docComment: string | null;
startLine: number;
endLine: number;
}
export interface TypeInfo {
name: string;
isExported: boolean;
definition: string;
docComment: string | null;
startLine: number;
endLine: number;
}
export interface ImportInfo {
source: string;
imports: Array<{ name: string; alias?: string }>;
isDefault: boolean;
startLine: number;
}
export interface ASTAnalysisResult {
filePath: string;
language: string;
functions: FunctionSignature[];
classes: ClassInfo[];
interfaces: InterfaceInfo[];
types: TypeInfo[];
imports: ImportInfo[];
exports: string[];
contentHash: string;
lastModified: string;
linesOfCode: number;
complexity: number;
}
export interface CodeDiff {
type: "added" | "removed" | "modified" | "unchanged";
category: "function" | "class" | "interface" | "type" | "import" | "export";
name: string;
details: string;
oldSignature?: string;
newSignature?: string;
impactLevel: "breaking" | "major" | "minor" | "patch";
}
/**
* Call graph node representing a function and its calls (Issue #72)
*/
export interface CallGraphNode {
/** Function signature with full metadata */
function: FunctionSignature;
/** File location of this function */
location: {
file: string;
line: number;
column?: number;
};
/** Child function calls made by this function */
calls: CallGraphNode[];
/** Conditional branches (if/else, switch) with their paths */
conditionalBranches: ConditionalPath[];
/** Exception types that can be raised */
exceptions: ExceptionPath[];
/** Current recursion depth */
depth: number;
/** Whether this node was truncated due to maxDepth */
truncated: boolean;
/** Whether this is an external/imported function */
isExternal: boolean;
/** Source of the import if external */
importSource?: string;
}
/**
* Conditional execution path (if/else, switch, ternary)
*/
export interface ConditionalPath {
/** Type of conditional */
type: "if" | "else-if" | "else" | "switch-case" | "ternary";
/** The condition expression as string */
condition: string;
/** Line number of the conditional */
lineNumber: number;
/** Functions called in the true/case branch */
trueBranch: CallGraphNode[];
/** Functions called in the false/else branch */
falseBranch: CallGraphNode[];
}
/**
* Exception path tracking
*/
export interface ExceptionPath {
/** Exception type/class being thrown */
exceptionType: string;
/** Line number of the throw statement */
lineNumber: number;
/** The throw expression as string */
expression: string;
/** Whether this is caught within the function */
isCaught: boolean;
}
/**
* Complete call graph for an entry point (Issue #72)
*/
export interface CallGraph {
/** Name of the entry point function */
entryPoint: string;
/** Root node of the call graph */
root: CallGraphNode;
/** All discovered functions in the call graph */
allFunctions: Map<string, FunctionSignature>;
/** Maximum depth that was actually reached */
maxDepthReached: number;
/** Files that were analyzed */
analyzedFiles: string[];
/** Circular references detected */
circularReferences: Array<{ from: string; to: string }>;
/** External calls that couldn't be resolved */
unresolvedCalls: Array<{
name: string;
location: { file: string; line: number };
}>;
/** Build timestamp */
buildTime: string;
}
/**
* Options for building call graphs
*/
export interface CallGraphOptions {
/** Maximum recursion depth (default: 3) */
maxDepth?: number;
/** Whether to resolve cross-file imports (default: true) */
resolveImports?: boolean;
/** Whether to extract conditional branches (default: true) */
extractConditionals?: boolean;
/** Whether to track exceptions (default: true) */
trackExceptions?: boolean;
/** File extensions to consider for import resolution */
extensions?: string[];
}
/**
* Main AST Analyzer class
*/
export class ASTAnalyzer {
private parsers: Map<string, any> = new Map();
private initialized = false;
/**
* Initialize tree-sitter parsers for all languages
*/
async initialize(): Promise<void> {
if (this.initialized) return;
// Note: Tree-sitter initialization would happen here in a full implementation
// For now, we're primarily using TypeScript/JavaScript parser
// console.log(
// "AST Analyzer initialized with language support:",
// Object.keys(LANGUAGE_CONFIGS),
// );
this.initialized = true;
}
/**
* Analyze a single file and extract AST information
*/
async analyzeFile(filePath: string): Promise<ASTAnalysisResult | null> {
if (!this.initialized) {
await this.initialize();
}
const ext = path.extname(filePath);
const language = this.detectLanguage(ext);
if (!language) {
console.warn(`Unsupported file extension: ${ext}`);
return null;
}
const content = await fs.readFile(filePath, "utf-8");
const stats = await fs.stat(filePath);
// Use TypeScript parser for .ts/.tsx files
if (language === "typescript" || language === "javascript") {
return this.analyzeTypeScript(
filePath,
content,
stats.mtime.toISOString(),
);
}
// For other languages, use tree-sitter (placeholder)
return this.analyzeWithTreeSitter(
filePath,
content,
language,
stats.mtime.toISOString(),
);
}
/**
* Analyze TypeScript/JavaScript using typescript-estree
*/
private async analyzeTypeScript(
filePath: string,
content: string,
lastModified: string,
): Promise<ASTAnalysisResult> {
const functions: FunctionSignature[] = [];
const classes: ClassInfo[] = [];
const interfaces: InterfaceInfo[] = [];
const types: TypeInfo[] = [];
const imports: ImportInfo[] = [];
const exports: string[] = [];
try {
const ast = parseTypeScript(content, {
loc: true,
range: true,
tokens: false,
comment: true,
jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
});
// Extract functions
this.extractFunctions(ast, content, functions);
// Extract classes
this.extractClasses(ast, content, classes);
// Extract interfaces
this.extractInterfaces(ast, content, interfaces);
// Extract type aliases
this.extractTypes(ast, content, types);
// Extract imports
this.extractImports(ast, imports);
// Extract exports
this.extractExports(ast, exports);
} catch (error) {
console.warn(`Failed to parse TypeScript file ${filePath}:`, error);
}
const contentHash = crypto
.createHash("sha256")
.update(content)
.digest("hex");
const linesOfCode = content.split("\n").length;
const complexity = this.calculateComplexity(functions, classes);
return {
filePath,
language:
filePath.endsWith(".ts") || filePath.endsWith(".tsx")
? "typescript"
: "javascript",
functions,
classes,
interfaces,
types,
imports,
exports,
contentHash,
lastModified,
linesOfCode,
complexity,
};
}
/**
* Analyze using tree-sitter (placeholder for other languages)
*/
private async analyzeWithTreeSitter(
filePath: string,
content: string,
language: string,
lastModified: string,
): Promise<ASTAnalysisResult> {
// Placeholder for tree-sitter analysis
// In a full implementation, we'd parse the content using tree-sitter
// and extract language-specific constructs
const contentHash = crypto
.createHash("sha256")
.update(content)
.digest("hex");
const linesOfCode = content.split("\n").length;
return {
filePath,
language,
functions: [],
classes: [],
interfaces: [],
types: [],
imports: [],
exports: [],
contentHash,
lastModified,
linesOfCode,
complexity: 0,
};
}
/**
* Extract function declarations from AST
*/
private extractFunctions(
ast: any,
content: string,
functions: FunctionSignature[],
): void {
const lines = content.split("\n");
const traverse = (node: any, isExported = false) => {
if (!node) return;
// Handle export declarations
if (
node.type === "ExportNamedDeclaration" ||
node.type === "ExportDefaultDeclaration"
) {
if (node.declaration) {
traverse(node.declaration, true);
}
return;
}
// Function declarations
if (node.type === "FunctionDeclaration") {
const func = this.parseFunctionNode(node, lines, isExported);
if (func) functions.push(func);
}
// Arrow functions assigned to variables
if (node.type === "VariableDeclaration") {
for (const declarator of node.declarations || []) {
if (declarator.init?.type === "ArrowFunctionExpression") {
const func = this.parseArrowFunction(declarator, lines, isExported);
if (func) functions.push(func);
}
}
}
// Traverse children
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child, false));
} else {
traverse(node[key], false);
}
}
}
};
traverse(ast);
}
/**
* Parse function node
*/
private parseFunctionNode(
node: any,
lines: string[],
isExported: boolean,
): FunctionSignature | null {
if (!node.id?.name) return null;
const docComment = this.extractDocComment(node.loc?.start.line - 1, lines);
const parameters = this.extractParameters(node.params);
return {
name: node.id.name,
parameters,
returnType: this.extractReturnType(node),
isAsync: node.async || false,
isExported,
isPublic: true,
docComment,
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
complexity: this.calculateFunctionComplexity(node),
dependencies: [],
};
}
/**
* Parse arrow function
*/
private parseArrowFunction(
declarator: any,
lines: string[],
isExported: boolean,
): FunctionSignature | null {
if (!declarator.id?.name) return null;
const node = declarator.init;
const docComment = this.extractDocComment(
declarator.loc?.start.line - 1,
lines,
);
const parameters = this.extractParameters(node.params);
return {
name: declarator.id.name,
parameters,
returnType: this.extractReturnType(node),
isAsync: node.async || false,
isExported,
isPublic: true,
docComment,
startLine: declarator.loc?.start.line || 0,
endLine: declarator.loc?.end.line || 0,
complexity: this.calculateFunctionComplexity(node),
dependencies: [],
};
}
/**
* Extract classes from AST
*/
private extractClasses(
ast: any,
content: string,
classes: ClassInfo[],
): void {
const lines = content.split("\n");
const traverse = (node: any, isExported = false) => {
if (!node) return;
// Handle export declarations
if (
node.type === "ExportNamedDeclaration" ||
node.type === "ExportDefaultDeclaration"
) {
if (node.declaration) {
traverse(node.declaration, true);
}
return;
}
if (node.type === "ClassDeclaration" && node.id?.name) {
const classInfo = this.parseClassNode(node, lines, isExported);
if (classInfo) classes.push(classInfo);
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child, false));
} else {
traverse(node[key], false);
}
}
}
};
traverse(ast);
}
/**
* Parse class node
*/
private parseClassNode(
node: any,
lines: string[],
isExported: boolean,
): ClassInfo | null {
const methods: FunctionSignature[] = [];
const properties: PropertyInfo[] = [];
// Extract methods and properties
if (node.body?.body) {
for (const member of node.body.body) {
if (member.type === "MethodDefinition") {
const method = this.parseMethodNode(member, lines);
if (method) methods.push(method);
} else if (member.type === "PropertyDefinition") {
const property = this.parsePropertyNode(member);
if (property) properties.push(property);
}
}
}
return {
name: node.id.name,
isExported,
extends: node.superClass?.name || null,
implements:
node.implements?.map((i: any) => i.expression?.name || "unknown") || [],
methods,
properties,
docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
};
}
/**
* Parse method node
*/
private parseMethodNode(
node: any,
lines: string[],
): FunctionSignature | null {
if (!node.key?.name) return null;
return {
name: node.key.name,
parameters: this.extractParameters(node.value?.params || []),
returnType: this.extractReturnType(node.value),
isAsync: node.value?.async || false,
isExported: false,
isPublic: !node.key.name.startsWith("_"),
docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
complexity: this.calculateFunctionComplexity(node.value),
dependencies: [],
};
}
/**
* Parse property node
*/
private parsePropertyNode(node: any): PropertyInfo | null {
if (!node.key?.name) return null;
return {
name: node.key.name,
type: this.extractTypeAnnotation(node.typeAnnotation),
isStatic: node.static || false,
isReadonly: node.readonly || false,
visibility: this.determineVisibility(node),
};
}
/**
* Extract interfaces from AST
*/
private extractInterfaces(
ast: any,
content: string,
interfaces: InterfaceInfo[],
): void {
const lines = content.split("\n");
const traverse = (node: any, isExported = false) => {
if (!node) return;
// Handle export declarations
if (
node.type === "ExportNamedDeclaration" ||
node.type === "ExportDefaultDeclaration"
) {
if (node.declaration) {
traverse(node.declaration, true);
}
return;
}
if (node.type === "TSInterfaceDeclaration" && node.id?.name) {
const interfaceInfo = this.parseInterfaceNode(node, lines, isExported);
if (interfaceInfo) interfaces.push(interfaceInfo);
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child, false));
} else {
traverse(node[key], false);
}
}
}
};
traverse(ast);
}
/**
* Parse interface node
*/
private parseInterfaceNode(
node: any,
lines: string[],
isExported: boolean,
): InterfaceInfo | null {
const properties: PropertyInfo[] = [];
const methods: FunctionSignature[] = [];
if (node.body?.body) {
for (const member of node.body.body) {
if (member.type === "TSPropertySignature") {
const prop = this.parseInterfaceProperty(member);
if (prop) properties.push(prop);
} else if (member.type === "TSMethodSignature") {
const method = this.parseInterfaceMethod(member);
if (method) methods.push(method);
}
}
}
return {
name: node.id.name,
isExported,
extends:
node.extends?.map((e: any) => e.expression?.name || "unknown") || [],
properties,
methods,
docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
};
}
/**
* Parse interface property
*/
private parseInterfaceProperty(node: any): PropertyInfo | null {
if (!node.key?.name) return null;
return {
name: node.key.name,
type: this.extractTypeAnnotation(node.typeAnnotation),
isStatic: false,
isReadonly: node.readonly || false,
visibility: "public",
};
}
/**
* Parse interface method
*/
private parseInterfaceMethod(node: any): FunctionSignature | null {
if (!node.key?.name) return null;
return {
name: node.key.name,
parameters: this.extractParameters(node.params || []),
returnType: this.extractTypeAnnotation(node.returnType),
isAsync: false,
isExported: false,
isPublic: true,
docComment: null,
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
complexity: 0,
dependencies: [],
};
}
/**
* Extract type aliases from AST
*/
private extractTypes(ast: any, content: string, types: TypeInfo[]): void {
const lines = content.split("\n");
const traverse = (node: any, isExported = false) => {
if (!node) return;
// Handle export declarations
if (
node.type === "ExportNamedDeclaration" ||
node.type === "ExportDefaultDeclaration"
) {
if (node.declaration) {
traverse(node.declaration, true);
}
return;
}
if (node.type === "TSTypeAliasDeclaration" && node.id?.name) {
const typeInfo = this.parseTypeNode(node, lines, isExported);
if (typeInfo) types.push(typeInfo);
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child, false));
} else {
traverse(node[key], false);
}
}
}
};
traverse(ast);
}
/**
* Parse type alias node
*/
private parseTypeNode(
node: any,
lines: string[],
isExported: boolean,
): TypeInfo | null {
return {
name: node.id.name,
isExported,
definition: this.extractTypeDefinition(node.typeAnnotation),
docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
startLine: node.loc?.start.line || 0,
endLine: node.loc?.end.line || 0,
};
}
/**
* Extract imports from AST
*/
private extractImports(ast: any, imports: ImportInfo[]): void {
const traverse = (node: any) => {
if (!node) return;
if (node.type === "ImportDeclaration") {
const importInfo: ImportInfo = {
source: node.source?.value || "",
imports: [],
isDefault: false,
startLine: node.loc?.start.line || 0,
};
for (const specifier of node.specifiers || []) {
if (specifier.type === "ImportDefaultSpecifier") {
importInfo.isDefault = true;
importInfo.imports.push({
name: specifier.local?.name || "default",
});
} else if (specifier.type === "ImportSpecifier") {
importInfo.imports.push({
name: specifier.imported?.name || "",
alias:
specifier.local?.name !== specifier.imported?.name
? specifier.local?.name
: undefined,
});
}
}
imports.push(importInfo);
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child));
} else {
traverse(node[key]);
}
}
}
};
traverse(ast);
}
/**
* Extract exports from AST
*/
private extractExports(ast: any, exports: string[]): void {
const traverse = (node: any) => {
if (!node) return;
// Named exports
if (node.type === "ExportNamedDeclaration") {
if (node.declaration) {
if (node.declaration.id?.name) {
exports.push(node.declaration.id.name);
} else if (node.declaration.declarations) {
for (const decl of node.declaration.declarations) {
if (decl.id?.name) exports.push(decl.id.name);
}
}
}
for (const specifier of node.specifiers || []) {
if (specifier.exported?.name) exports.push(specifier.exported.name);
}
}
// Default export
if (node.type === "ExportDefaultDeclaration") {
if (node.declaration?.id?.name) {
exports.push(node.declaration.id.name);
} else {
exports.push("default");
}
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child));
} else {
traverse(node[key]);
}
}
}
};
traverse(ast);
}
// Helper methods
private extractParameters(params: any[]): ParameterInfo[] {
return params.map((param) => ({
name: param.name || param.argument?.name || param.left?.name || "unknown",
type: this.extractTypeAnnotation(param.typeAnnotation),
optional: param.optional || false,
defaultValue: param.right ? this.extractDefaultValue(param.right) : null,
}));
}
private extractReturnType(node: any): string | null {
return this.extractTypeAnnotation(node?.returnType);
}
private extractTypeAnnotation(typeAnnotation: any): string | null {
if (!typeAnnotation) return null;
if (typeAnnotation.typeAnnotation)
return this.extractTypeDefinition(typeAnnotation.typeAnnotation);
return this.extractTypeDefinition(typeAnnotation);
}
private extractTypeDefinition(typeNode: any): string {
if (!typeNode) return "unknown";
if (typeNode.type === "TSStringKeyword") return "string";
if (typeNode.type === "TSNumberKeyword") return "number";
if (typeNode.type === "TSBooleanKeyword") return "boolean";
if (typeNode.type === "TSAnyKeyword") return "any";
if (typeNode.type === "TSVoidKeyword") return "void";
if (typeNode.type === "TSTypeReference")
return typeNode.typeName?.name || "unknown";
return "unknown";
}
private extractDefaultValue(node: any): string | null {
if (node.type === "Literal") return String(node.value);
if (node.type === "Identifier") return node.name;
return null;
}
private extractDocComment(
lineNumber: number,
lines: string[],
): string | null {
if (lineNumber < 0 || lineNumber >= lines.length) return null;
const comment: string[] = [];
let currentLine = lineNumber;
// Look backwards for JSDoc comment
while (currentLine >= 0) {
const line = lines[currentLine].trim();
if (line.startsWith("*/")) {
comment.unshift(line);
currentLine--;
continue;
}
if (line.startsWith("*") || line.startsWith("/**")) {
comment.unshift(line);
if (line.startsWith("/**")) break;
currentLine--;
continue;
}
if (comment.length > 0) break;
currentLine--;
}
return comment.length > 0 ? comment.join("\n") : null;
}
private isExported(node: any): boolean {
if (!node) return false;
// Check parent for export
let current = node;
while (current) {
if (
current.type === "ExportNamedDeclaration" ||
current.type === "ExportDefaultDeclaration"
) {
return true;
}
current = current.parent;
}
return false;
}
private determineVisibility(node: any): "public" | "private" | "protected" {
if (node.accessibility) return node.accessibility;
if (node.key?.name?.startsWith("_")) return "private";
if (node.key?.name?.startsWith("#")) return "private";
return "public";
}
private calculateFunctionComplexity(node: any): number {
// Simplified cyclomatic complexity
let complexity = 1;
const traverse = (n: any) => {
if (!n) return;
// Increment for control flow statements
if (
[
"IfStatement",
"ConditionalExpression",
"ForStatement",
"WhileStatement",
"DoWhileStatement",
"SwitchCase",
"CatchClause",
].includes(n.type)
) {
complexity++;
}
for (const key in n) {
if (typeof n[key] === "object" && n[key] !== null) {
if (Array.isArray(n[key])) {
n[key].forEach((child: any) => traverse(child));
} else {
traverse(n[key]);
}
}
}
};
traverse(node);
return complexity;
}
private calculateComplexity(
functions: FunctionSignature[],
classes: ClassInfo[],
): number {
const functionComplexity = functions.reduce(
(sum, f) => sum + f.complexity,
0,
);
const classComplexity = classes.reduce(
(sum, c) =>
sum + c.methods.reduce((methodSum, m) => methodSum + m.complexity, 0),
0,
);
return functionComplexity + classComplexity;
}
private detectLanguage(ext: string): string | null {
for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
if (config.extensions.includes(ext)) return lang;
}
return null;
}
/**
* Compare two AST analysis results and detect changes
*/
async detectDrift(
oldAnalysis: ASTAnalysisResult,
newAnalysis: ASTAnalysisResult,
): Promise<CodeDiff[]> {
const diffs: CodeDiff[] = [];
// Compare functions
diffs.push(
...this.compareFunctions(oldAnalysis.functions, newAnalysis.functions),
);
// Compare classes
diffs.push(
...this.compareClasses(oldAnalysis.classes, newAnalysis.classes),
);
// Compare interfaces
diffs.push(
...this.compareInterfaces(oldAnalysis.interfaces, newAnalysis.interfaces),
);
// Compare types
diffs.push(...this.compareTypes(oldAnalysis.types, newAnalysis.types));
return diffs;
}
private compareFunctions(
oldFuncs: FunctionSignature[],
newFuncs: FunctionSignature[],
): CodeDiff[] {
const diffs: CodeDiff[] = [];
const oldMap = new Map(oldFuncs.map((f) => [f.name, f]));
const newMap = new Map(newFuncs.map((f) => [f.name, f]));
// Check for removed functions
for (const [name, func] of oldMap) {
if (!newMap.has(name)) {
diffs.push({
type: "removed",
category: "function",
name,
details: `Function '${name}' was removed`,
oldSignature: this.formatFunctionSignature(func),
impactLevel: func.isExported ? "breaking" : "minor",
});
}
}
// Check for added functions
for (const [name, func] of newMap) {
if (!oldMap.has(name)) {
diffs.push({
type: "added",
category: "function",
name,
details: `Function '${name}' was added`,
newSignature: this.formatFunctionSignature(func),
impactLevel: "patch",
});
}
}
// Check for modified functions
for (const [name, newFunc] of newMap) {
const oldFunc = oldMap.get(name);
if (oldFunc) {
const changes = this.detectFunctionChanges(oldFunc, newFunc);
if (changes.length > 0) {
diffs.push({
type: "modified",
category: "function",
name,
details: changes.join("; "),
oldSignature: this.formatFunctionSignature(oldFunc),
newSignature: this.formatFunctionSignature(newFunc),
impactLevel: this.determineFunctionImpact(oldFunc, newFunc),
});
}
}
}
return diffs;
}
private compareClasses(
oldClasses: ClassInfo[],
newClasses: ClassInfo[],
): CodeDiff[] {
const diffs: CodeDiff[] = [];
const oldMap = new Map(oldClasses.map((c) => [c.name, c]));
const newMap = new Map(newClasses.map((c) => [c.name, c]));
for (const [name, oldClass] of oldMap) {
if (!newMap.has(name)) {
diffs.push({
type: "removed",
category: "class",
name,
details: `Class '${name}' was removed`,
impactLevel: oldClass.isExported ? "breaking" : "minor",
});
}
}
for (const [name] of newMap) {
if (!oldMap.has(name)) {
diffs.push({
type: "added",
category: "class",
name,
details: `Class '${name}' was added`,
impactLevel: "patch",
});
}
}
return diffs;
}
private compareInterfaces(
oldInterfaces: InterfaceInfo[],
newInterfaces: InterfaceInfo[],
): CodeDiff[] {
const diffs: CodeDiff[] = [];
const oldMap = new Map(oldInterfaces.map((i) => [i.name, i]));
const newMap = new Map(newInterfaces.map((i) => [i.name, i]));
for (const [name, oldInterface] of oldMap) {
if (!newMap.has(name)) {
diffs.push({
type: "removed",
category: "interface",
name,
details: `Interface '${name}' was removed`,
impactLevel: oldInterface.isExported ? "breaking" : "minor",
});
}
}
for (const [name] of newMap) {
if (!oldMap.has(name)) {
diffs.push({
type: "added",
category: "interface",
name,
details: `Interface '${name}' was added`,
impactLevel: "patch",
});
}
}
return diffs;
}
private compareTypes(oldTypes: TypeInfo[], newTypes: TypeInfo[]): CodeDiff[] {
const diffs: CodeDiff[] = [];
const oldMap = new Map(oldTypes.map((t) => [t.name, t]));
const newMap = new Map(newTypes.map((t) => [t.name, t]));
for (const [name, oldType] of oldMap) {
if (!newMap.has(name)) {
diffs.push({
type: "removed",
category: "type",
name,
details: `Type '${name}' was removed`,
impactLevel: oldType.isExported ? "breaking" : "minor",
});
}
}
for (const [name] of newMap) {
if (!oldMap.has(name)) {
diffs.push({
type: "added",
category: "type",
name,
details: `Type '${name}' was added`,
impactLevel: "patch",
});
}
}
return diffs;
}
private detectFunctionChanges(
oldFunc: FunctionSignature,
newFunc: FunctionSignature,
): string[] {
const changes: string[] = [];
// Check parameter changes
if (oldFunc.parameters.length !== newFunc.parameters.length) {
changes.push(
`Parameter count changed from ${oldFunc.parameters.length} to ${newFunc.parameters.length}`,
);
}
// Check return type changes
if (oldFunc.returnType !== newFunc.returnType) {
changes.push(
`Return type changed from '${oldFunc.returnType}' to '${newFunc.returnType}'`,
);
}
// Check async changes
if (oldFunc.isAsync !== newFunc.isAsync) {
changes.push(
newFunc.isAsync
? "Function became async"
: "Function is no longer async",
);
}
// Check export changes
if (oldFunc.isExported !== newFunc.isExported) {
changes.push(
newFunc.isExported
? "Function is now exported"
: "Function is no longer exported",
);
}
return changes;
}
private determineFunctionImpact(
oldFunc: FunctionSignature,
newFunc: FunctionSignature,
): "breaking" | "major" | "minor" | "patch" {
// Breaking changes
if (oldFunc.isExported) {
if (oldFunc.parameters.length !== newFunc.parameters.length)
return "breaking";
if (oldFunc.returnType !== newFunc.returnType) return "breaking";
// If a function was exported and is no longer exported, that's breaking
if (oldFunc.isExported && !newFunc.isExported) return "breaking";
}
// Major changes
if (oldFunc.isAsync !== newFunc.isAsync) return "major";
// Minor changes (new API surface)
// If a function becomes exported, that's a minor change (new feature/API)
if (!oldFunc.isExported && newFunc.isExported) return "minor";
return "patch";
}
private formatFunctionSignature(func: FunctionSignature): string {
const params = func.parameters
.map((p) => `${p.name}: ${p.type || "any"}`)
.join(", ");
const returnType = func.returnType || "void";
const asyncPrefix = func.isAsync ? "async " : "";
return `${asyncPrefix}${func.name}(${params}): ${returnType}`;
}
// ============================================================================
// Call Graph Builder (Issue #72)
// ============================================================================
/**
* Build a call graph starting from an entry function
*
* @param entryFunction - Name of the function to start from
* @param projectPath - Root path of the project for cross-file resolution
* @param options - Call graph building options
* @returns Complete call graph with all discovered paths
*/
async buildCallGraph(
entryFunction: string,
projectPath: string,
options: CallGraphOptions = {},
): Promise<CallGraph> {
const resolvedOptions: Required<CallGraphOptions> = {
maxDepth: options.maxDepth ?? 3,
resolveImports: options.resolveImports ?? true,
extractConditionals: options.extractConditionals ?? true,
trackExceptions: options.trackExceptions ?? true,
extensions: options.extensions ?? [".ts", ".tsx", ".js", ".jsx", ".mjs"],
};
if (!this.initialized) {
await this.initialize();
}
// Find the entry file containing the function
const entryFile = await this.findFunctionFile(
entryFunction,
projectPath,
resolvedOptions.extensions,
);
if (!entryFile) {
return this.createEmptyCallGraph(entryFunction);
}
// Analyze the entry file
const entryAnalysis = await this.analyzeFile(entryFile);
if (!entryAnalysis) {
return this.createEmptyCallGraph(entryFunction);
}
const entryFunc = entryAnalysis.functions.find(
(f) => f.name === entryFunction,
);
if (!entryFunc) {
// Check class methods
for (const cls of entryAnalysis.classes) {
const method = cls.methods.find((m) => m.name === entryFunction);
if (method) {
return this.buildCallGraphFromFunction(
method,
entryFile,
entryAnalysis,
projectPath,
resolvedOptions,
);
}
}
return this.createEmptyCallGraph(entryFunction);
}
return this.buildCallGraphFromFunction(
entryFunc,
entryFile,
entryAnalysis,
projectPath,
resolvedOptions,
);
}
/**
* Build call graph from a specific function
*/
private async buildCallGraphFromFunction(
entryFunc: FunctionSignature,
entryFile: string,
entryAnalysis: ASTAnalysisResult,
projectPath: string,
options: Required<CallGraphOptions>,
): Promise<CallGraph> {
const allFunctions = new Map<string, FunctionSignature>();
const analyzedFiles: string[] = [entryFile];
const circularReferences: Array<{ from: string; to: string }> = [];
const unresolvedCalls: Array<{
name: string;
location: { file: string; line: number };
}> = [];
// Cache for analyzed files
const analysisCache = new Map<string, ASTAnalysisResult>();
analysisCache.set(entryFile, entryAnalysis);
// Track visited functions to prevent infinite loops
const visited = new Set<string>();
let maxDepthReached = 0;
const root = await this.buildCallGraphNode(
entryFunc,
entryFile,
entryAnalysis,
projectPath,
options,
0,
visited,
allFunctions,
analysisCache,
circularReferences,
unresolvedCalls,
analyzedFiles,
(depth) => {
maxDepthReached = Math.max(maxDepthReached, depth);
},
);
return {
entryPoint: entryFunc.name,
root,
allFunctions,
maxDepthReached,
analyzedFiles: [...new Set(analyzedFiles)],
circularReferences,
unresolvedCalls,
buildTime: new Date().toISOString(),
};
}
/**
* Build a single call graph node recursively
*/
private async buildCallGraphNode(
func: FunctionSignature,
filePath: string,
analysis: ASTAnalysisResult,
projectPath: string,
options: Required<CallGraphOptions>,
depth: number,
visited: Set<string>,
allFunctions: Map<string, FunctionSignature>,
analysisCache: Map<string, ASTAnalysisResult>,
circularReferences: Array<{ from: string; to: string }>,
unresolvedCalls: Array<{
name: string;
location: { file: string; line: number };
}>,
analyzedFiles: string[],
updateMaxDepth: (depth: number) => void,
): Promise<CallGraphNode> {
const funcKey = `${filePath}:${func.name}`;
updateMaxDepth(depth);
// Check for circular reference
if (visited.has(funcKey)) {
circularReferences.push({ from: funcKey, to: func.name });
return this.createTruncatedNode(func, filePath, depth, true);
}
// Check max depth
if (depth >= options.maxDepth) {
return this.createTruncatedNode(func, filePath, depth, false);
}
visited.add(funcKey);
allFunctions.set(func.name, func);
// Read the file content to extract function body
let content: string;
try {
content = await fs.readFile(filePath, "utf-8");
} catch {
return this.createTruncatedNode(func, filePath, depth, false);
}
// Parse the function body to find calls, conditionals, and exceptions
const functionBody = this.extractFunctionBody(content, func);
const callExpressions = this.extractCallExpressions(
functionBody,
func.startLine,
);
const conditionalBranches: ConditionalPath[] = options.extractConditionals
? await this.extractConditionalPaths(
functionBody,
func.startLine,
analysis,
filePath,
projectPath,
options,
depth,
visited,
allFunctions,
analysisCache,
circularReferences,
unresolvedCalls,
analyzedFiles,
updateMaxDepth,
)
: [];
const exceptions: ExceptionPath[] = options.trackExceptions
? this.extractExceptions(functionBody, func.startLine)
: [];
// Build child nodes for each call
const calls: CallGraphNode[] = [];
for (const call of callExpressions) {
const childNode = await this.resolveAndBuildChildNode(
call,
filePath,
analysis,
projectPath,
options,
depth + 1,
visited,
allFunctions,
analysisCache,
circularReferences,
unresolvedCalls,
analyzedFiles,
updateMaxDepth,
);
if (childNode) {
calls.push(childNode);
}
}
visited.delete(funcKey); // Allow revisiting from different paths
return {
function: func,
location: {
file: filePath,
line: func.startLine,
},
calls,
conditionalBranches,
exceptions,
depth,
truncated: false,
isExternal: false,
};
}
/**
* Resolve a function call and build its child node
*/
private async resolveAndBuildChildNode(
call: { name: string; line: number; isMethod: boolean; object?: string },
currentFile: string,
currentAnalysis: ASTAnalysisResult,
projectPath: string,
options: Required<CallGraphOptions>,
depth: number,
visited: Set<string>,
allFunctions: Map<string, FunctionSignature>,
analysisCache: Map<string, ASTAnalysisResult>,
circularReferences: Array<{ from: string; to: string }>,
unresolvedCalls: Array<{
name: string;
location: { file: string; line: number };
}>,
analyzedFiles: string[],
updateMaxDepth: (depth: number) => void,
): Promise<CallGraphNode | null> {
// First, try to find the function in the current file
let targetFunc = currentAnalysis.functions.find(
(f) => f.name === call.name,
);
let targetFile = currentFile;
let targetAnalysis = currentAnalysis;
// Check class methods if it's a method call
if (!targetFunc && call.isMethod) {
for (const cls of currentAnalysis.classes) {
const method = cls.methods.find((m) => m.name === call.name);
if (method) {
targetFunc = method;
break;
}
}
}
// If not found locally, try to resolve from imports
if (!targetFunc && options.resolveImports) {
const resolvedImport = await this.resolveImportedFunction(
call.name,
currentAnalysis.imports,
currentFile,
projectPath,
options.extensions,
analysisCache,
analyzedFiles,
);
if (resolvedImport) {
targetFunc = resolvedImport.func;
targetFile = resolvedImport.file;
targetAnalysis = resolvedImport.analysis;
}
}
if (!targetFunc) {
// Track as unresolved call (might be built-in or external library)
if (!this.isBuiltInFunction(call.name)) {
unresolvedCalls.push({
name: call.name,
location: { file: currentFile, line: call.line },
});
}
return null;
}
return this.buildCallGraphNode(
targetFunc,
targetFile,
targetAnalysis,
projectPath,
options,
depth,
visited,
allFunctions,
analysisCache,
circularReferences,
unresolvedCalls,
analyzedFiles,
updateMaxDepth,
);
}
/**
* Resolve an imported function to its source file
*/
private async resolveImportedFunction(
funcName: string,
imports: ImportInfo[],
currentFile: string,
projectPath: string,
extensions: string[],
analysisCache: Map<string, ASTAnalysisResult>,
analyzedFiles: string[],
): Promise<{
func: FunctionSignature;
file: string;
analysis: ASTAnalysisResult;
} | null> {
// Find the import that provides this function
for (const imp of imports) {
const importedItem = imp.imports.find(
(i) => i.name === funcName || i.alias === funcName,
);
if (
importedItem ||
(imp.isDefault && imp.imports[0]?.name === funcName)
) {
// Resolve the import path
const resolvedPath = await this.resolveImportPath(
imp.source,
currentFile,
projectPath,
extensions,
);
if (!resolvedPath) continue;
// Check cache first
let analysis: ASTAnalysisResult | undefined =
analysisCache.get(resolvedPath);
if (!analysis) {
const analyzedFile = await this.analyzeFile(resolvedPath);
if (analyzedFile) {
analysis = analyzedFile;
analysisCache.set(resolvedPath, analysis);
analyzedFiles.push(resolvedPath);
}
}
if (!analysis) continue;
// Find the function in the resolved file
const func = analysis.functions.find(
(f) => f.name === (importedItem?.name || funcName),
);
if (func) {
return { func, file: resolvedPath, analysis };
}
// Check class methods
for (const cls of analysis.classes) {
const method = cls.methods.find(
(m) => m.name === (importedItem?.name || funcName),
);
if (method) {
return { func: method, file: resolvedPath, analysis };
}
}
}
}
return null;
}
/**
* Resolve an import path to an absolute file path
*/
private async resolveImportPath(
importSource: string,
currentFile: string,
projectPath: string,
extensions: string[],
): Promise<string | null> {
// Skip node_modules and external packages
if (
!importSource.startsWith(".") &&
!importSource.startsWith("/") &&
!importSource.startsWith("@/")
) {
return null;
}
const currentDir = path.dirname(currentFile);
let basePath: string;
if (importSource.startsWith("@/")) {
// Handle alias imports (common in Next.js, etc.)
basePath = path.join(projectPath, importSource.slice(2));
} else {
basePath = path.resolve(currentDir, importSource);
}
// Try with different extensions
const candidates = [
basePath,
...extensions.map((ext) => basePath + ext),
path.join(basePath, "index.ts"),
path.join(basePath, "index.tsx"),
path.join(basePath, "index.js"),
];
for (const candidate of candidates) {
try {
await fs.access(candidate);
return candidate;
} catch {
// File doesn't exist, try next
}
}
return null;
}
/**
* Find the file containing a function
*/
private async findFunctionFile(
funcName: string,
projectPath: string,
extensions: string[],
): Promise<string | null> {
// Search common source directories
const searchDirs = ["src", "lib", "app", "."];
for (const dir of searchDirs) {
const searchPath = path.join(projectPath, dir);
try {
const files = await this.findFilesRecursive(searchPath, extensions);
for (const file of files) {
const analysis = await this.analyzeFile(file);
if (analysis) {
const func = analysis.functions.find((f) => f.name === funcName);
if (func) return file;
// Check class methods
for (const cls of analysis.classes) {
if (cls.methods.find((m) => m.name === funcName)) {
return file;
}
}
}
}
} catch {
// Directory doesn't exist, continue
}
}
return null;
}
/**
* Recursively find files with given extensions
*/
private async findFilesRecursive(
dir: string,
extensions: string[],
maxDepth: number = 5,
currentDepth: number = 0,
): Promise<string[]> {
if (currentDepth >= maxDepth) return [];
const files: string[] = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip node_modules and hidden directories
if (
entry.name === "node_modules" ||
entry.name.startsWith(".") ||
entry.name === "dist" ||
entry.name === "build"
) {
continue;
}
if (entry.isDirectory()) {
files.push(
...(await this.findFilesRecursive(
fullPath,
extensions,
maxDepth,
currentDepth + 1,
)),
);
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
} catch {
// Directory access error
}
return files;
}
/**
* Extract the body of a function from source code
*/
private extractFunctionBody(
content: string,
func: FunctionSignature,
): string {
const lines = content.split("\n");
return lines.slice(func.startLine - 1, func.endLine).join("\n");
}
/**
* Extract function call expressions from code
*/
private extractCallExpressions(
code: string,
startLine: number,
): Array<{ name: string; line: number; isMethod: boolean; object?: string }> {
const calls: Array<{
name: string;
line: number;
isMethod: boolean;
object?: string;
}> = [];
try {
const ast = parseTypeScript(code, {
loc: true,
range: true,
tokens: false,
comment: false,
});
const traverse = (node: any) => {
if (!node) return;
if (node.type === "CallExpression") {
const callee = node.callee;
const line = (node.loc?.start.line || 0) + startLine - 1;
if (callee.type === "Identifier") {
calls.push({
name: callee.name,
line,
isMethod: false,
});
} else if (callee.type === "MemberExpression") {
if (callee.property?.name) {
calls.push({
name: callee.property.name,
line,
isMethod: true,
object: callee.object?.name,
});
}
}
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child));
} else {
traverse(node[key]);
}
}
}
};
traverse(ast);
} catch {
// Parse error, return empty
}
return calls;
}
/**
* Extract conditional paths from function body
*/
private async extractConditionalPaths(
code: string,
startLine: number,
currentAnalysis: ASTAnalysisResult,
currentFile: string,
projectPath: string,
options: Required<CallGraphOptions>,
depth: number,
visited: Set<string>,
allFunctions: Map<string, FunctionSignature>,
analysisCache: Map<string, ASTAnalysisResult>,
circularReferences: Array<{ from: string; to: string }>,
unresolvedCalls: Array<{
name: string;
location: { file: string; line: number };
}>,
analyzedFiles: string[],
updateMaxDepth: (depth: number) => void,
): Promise<ConditionalPath[]> {
const conditionals: ConditionalPath[] = [];
try {
const ast = parseTypeScript(code, {
loc: true,
range: true,
tokens: false,
comment: false,
});
const extractConditionString = (node: any): string => {
if (!node) return "unknown";
if (node.type === "Identifier") return node.name;
if (node.type === "BinaryExpression") {
return `${extractConditionString(node.left)} ${
node.operator
} ${extractConditionString(node.right)}`;
}
if (node.type === "MemberExpression") {
return `${extractConditionString(node.object)}.${
node.property?.name || "?"
}`;
}
if (node.type === "UnaryExpression") {
return `${node.operator}${extractConditionString(node.argument)}`;
}
if (node.type === "Literal") {
return String(node.value);
}
return "complex";
};
const extractBranchCalls = async (
branchNode: any,
): Promise<CallGraphNode[]> => {
if (!branchNode) return [];
const branchCode =
branchNode.type === "BlockStatement"
? code.slice(branchNode.range[0], branchNode.range[1])
: code.slice(
branchNode.range?.[0] || 0,
branchNode.range?.[1] || 0,
);
const branchCalls = this.extractCallExpressions(
branchCode,
(branchNode.loc?.start.line || 0) + startLine - 1,
);
const nodes: CallGraphNode[] = [];
for (const call of branchCalls) {
const childNode = await this.resolveAndBuildChildNode(
call,
currentFile,
currentAnalysis,
projectPath,
options,
depth + 1,
visited,
allFunctions,
analysisCache,
circularReferences,
unresolvedCalls,
analyzedFiles,
updateMaxDepth,
);
if (childNode) {
nodes.push(childNode);
}
}
return nodes;
};
const traverse = async (node: any) => {
if (!node) return;
// If statement
if (node.type === "IfStatement") {
const condition = extractConditionString(node.test);
const line = (node.loc?.start.line || 0) + startLine - 1;
conditionals.push({
type: "if",
condition,
lineNumber: line,
trueBranch: await extractBranchCalls(node.consequent),
falseBranch: await extractBranchCalls(node.alternate),
});
}
// Switch statement
if (node.type === "SwitchStatement") {
const discriminant = extractConditionString(node.discriminant);
for (const switchCase of node.cases || []) {
conditionals.push({
type: "switch-case",
condition: switchCase.test
? `${discriminant} === ${extractConditionString(
switchCase.test,
)}`
: "default",
lineNumber: (switchCase.loc?.start.line || 0) + startLine - 1,
trueBranch: await extractBranchCalls(switchCase),
falseBranch: [],
});
}
}
// Ternary operator
if (node.type === "ConditionalExpression") {
const condition = extractConditionString(node.test);
const line = (node.loc?.start.line || 0) + startLine - 1;
conditionals.push({
type: "ternary",
condition,
lineNumber: line,
trueBranch: await extractBranchCalls(node.consequent),
falseBranch: await extractBranchCalls(node.alternate),
});
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
for (const child of node[key]) {
await traverse(child);
}
} else {
await traverse(node[key]);
}
}
}
};
await traverse(ast);
} catch {
// Parse error
}
return conditionals;
}
/**
* Extract exception paths (throw statements)
*/
private extractExceptions(code: string, startLine: number): ExceptionPath[] {
const exceptions: ExceptionPath[] = [];
try {
const ast = parseTypeScript(code, {
loc: true,
range: true,
tokens: false,
comment: false,
});
// Track try-catch blocks to determine if throws are caught
const catchRanges: Array<[number, number]> = [];
const collectCatchBlocks = (node: any) => {
if (!node) return;
if (node.type === "TryStatement" && node.handler) {
const handlerRange = node.handler.range || [0, 0];
catchRanges.push(handlerRange);
}
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => collectCatchBlocks(child));
} else {
collectCatchBlocks(node[key]);
}
}
}
};
const traverse = (node: any, inTryBlock = false) => {
if (!node) return;
if (node.type === "TryStatement") {
traverse(node.block, true);
if (node.handler) traverse(node.handler.body, false);
if (node.finalizer) traverse(node.finalizer, false);
return;
}
if (node.type === "ThrowStatement") {
const line = (node.loc?.start.line || 0) + startLine - 1;
const argument = node.argument;
let exceptionType = "Error";
let expression = "unknown";
if (argument?.type === "NewExpression") {
exceptionType = argument.callee?.name || "Error";
expression = `new ${exceptionType}(...)`;
} else if (argument?.type === "Identifier") {
exceptionType = argument.name;
expression = argument.name;
} else if (argument?.type === "CallExpression") {
exceptionType = argument.callee?.name || "Error";
expression = `${exceptionType}(...)`;
}
exceptions.push({
exceptionType,
lineNumber: line,
expression,
isCaught: inTryBlock,
});
}
for (const key in node) {
if (
key !== "handler" &&
key !== "finalizer" &&
typeof node[key] === "object" &&
node[key] !== null
) {
if (Array.isArray(node[key])) {
node[key].forEach((child: any) => traverse(child, inTryBlock));
} else {
traverse(node[key], inTryBlock);
}
}
}
};
collectCatchBlocks(ast);
traverse(ast);
} catch {
// Parse error
}
return exceptions;
}
/**
* Check if a function name is a built-in JavaScript function
*/
private isBuiltInFunction(name: string): boolean {
const builtIns = new Set([
// Console
"log",
"warn",
"error",
"info",
"debug",
"trace",
"table",
// Array methods
"map",
"filter",
"reduce",
"forEach",
"find",
"findIndex",
"some",
"every",
"includes",
"indexOf",
"push",
"pop",
"shift",
"unshift",
"slice",
"splice",
"concat",
"join",
"sort",
"reverse",
"flat",
"flatMap",
// String methods
"split",
"trim",
"toLowerCase",
"toUpperCase",
"replace",
"substring",
"substr",
"charAt",
"startsWith",
"endsWith",
"padStart",
"padEnd",
"repeat",
// Object methods
"keys",
"values",
"entries",
"assign",
"freeze",
"seal",
"hasOwnProperty",
// Math methods
"max",
"min",
"abs",
"floor",
"ceil",
"round",
"random",
"sqrt",
"pow",
// JSON
"stringify",
"parse",
// Promise
"then",
"catch",
"finally",
"resolve",
"reject",
"all",
"race",
"allSettled",
// Timers
"setTimeout",
"setInterval",
"clearTimeout",
"clearInterval",
// Common
"require",
"import",
"console",
"Date",
"Error",
"Promise",
"fetch",
]);
return builtIns.has(name);
}
/**
* Create an empty call graph for when entry function is not found
*/
private createEmptyCallGraph(entryFunction: string): CallGraph {
return {
entryPoint: entryFunction,
root: {
function: {
name: entryFunction,
parameters: [],
returnType: null,
isAsync: false,
isExported: false,
isPublic: true,
docComment: null,
startLine: 0,
endLine: 0,
complexity: 0,
dependencies: [],
},
location: { file: "unknown", line: 0 },
calls: [],
conditionalBranches: [],
exceptions: [],
depth: 0,
truncated: false,
isExternal: true,
},
allFunctions: new Map(),
maxDepthReached: 0,
analyzedFiles: [],
circularReferences: [],
unresolvedCalls: [
{
name: entryFunction,
location: { file: "unknown", line: 0 },
},
],
buildTime: new Date().toISOString(),
};
}
/**
* Create a truncated node when max depth is reached or circular reference detected
*/
private createTruncatedNode(
func: FunctionSignature,
filePath: string,
depth: number,
_isCircular: boolean,
): CallGraphNode {
return {
function: func,
location: {
file: filePath,
line: func.startLine,
},
calls: [],
conditionalBranches: [],
exceptions: [],
depth,
truncated: true,
isExternal: false,
};
}
}
```