This is page 9 of 20. 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
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│ ├── .docusaurus
│ │ ├── docusaurus-plugin-content-docs
│ │ │ └── default
│ │ │ └── __mdx-loader-dependency.json
│ │ └── docusaurus-plugin-content-pages
│ │ └── default
│ │ └── __plugin.json
│ ├── adrs
│ │ ├── 001-mcp-server-architecture.md
│ │ ├── 002-repository-analysis-engine.md
│ │ ├── 003-static-site-generator-recommendation-engine.md
│ │ ├── 004-diataxis-framework-integration.md
│ │ ├── 005-github-pages-deployment-automation.md
│ │ ├── 006-mcp-tools-api-design.md
│ │ ├── 007-mcp-prompts-and-resources-integration.md
│ │ ├── 008-intelligent-content-population-engine.md
│ │ ├── 009-content-accuracy-validation-framework.md
│ │ ├── 010-mcp-resource-pattern-redesign.md
│ │ └── README.md
│ ├── api
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── hierarchy.js
│ │ │ ├── highlight.css
│ │ │ ├── icons.js
│ │ │ ├── icons.svg
│ │ │ ├── main.js
│ │ │ ├── navigation.js
│ │ │ ├── search.js
│ │ │ └── style.css
│ │ ├── hierarchy.html
│ │ ├── index.html
│ │ ├── modules.html
│ │ └── variables
│ │ └── TOOLS.html
│ ├── assets
│ │ └── logo.svg
│ ├── development
│ │ └── MCP_INSPECTOR_TESTING.md
│ ├── docusaurus.config.js
│ ├── explanation
│ │ ├── architecture.md
│ │ └── index.md
│ ├── guides
│ │ ├── link-validation.md
│ │ ├── playwright-integration.md
│ │ └── playwright-testing-workflow.md
│ ├── how-to
│ │ ├── analytics-setup.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── local-testing.md
│ │ ├── performance-optimization.md
│ │ ├── prompting-guide.md
│ │ ├── repository-analysis.md
│ │ ├── seo-optimization.md
│ │ ├── site-monitoring.md
│ │ ├── troubleshooting.md
│ │ └── usage-examples.md
│ ├── index.md
│ ├── knowledge-graph.md
│ ├── package-lock.json
│ ├── package.json
│ ├── phase-2-intelligence.md
│ ├── reference
│ │ ├── api-overview.md
│ │ ├── cli.md
│ │ ├── configuration.md
│ │ ├── deploy-pages.md
│ │ ├── index.md
│ │ ├── mcp-tools.md
│ │ └── prompt-templates.md
│ ├── research
│ │ ├── cross-domain-integration
│ │ │ └── README.md
│ │ ├── domain-1-mcp-architecture
│ │ │ ├── index.md
│ │ │ └── mcp-performance-research.md
│ │ ├── domain-2-repository-analysis
│ │ │ └── README.md
│ │ ├── domain-3-ssg-recommendation
│ │ │ ├── index.md
│ │ │ └── ssg-performance-analysis.md
│ │ ├── domain-4-diataxis-integration
│ │ │ └── README.md
│ │ ├── domain-5-github-deployment
│ │ │ ├── github-pages-security-analysis.md
│ │ │ └── index.md
│ │ ├── domain-6-api-design
│ │ │ └── README.md
│ │ ├── README.md
│ │ ├── research-integration-summary-2025-01-14.md
│ │ ├── research-progress-template.md
│ │ └── research-questions-2025-01-14.md
│ ├── robots.txt
│ ├── sidebars.js
│ ├── sitemap.xml
│ ├── src
│ │ └── css
│ │ └── custom.css
│ └── tutorials
│ ├── development-setup.md
│ ├── environment-setup.md
│ ├── first-deployment.md
│ ├── getting-started.md
│ ├── index.md
│ ├── memory-workflows.md
│ └── user-onboarding.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│ └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│ ├── benchmarks
│ │ └── performance.ts
│ ├── index.ts
│ ├── memory
│ │ ├── contextual-retrieval.ts
│ │ ├── deployment-analytics.ts
│ │ ├── enhanced-manager.ts
│ │ ├── export-import.ts
│ │ ├── freshness-kg-integration.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kg-code-integration.ts
│ │ ├── kg-health.ts
│ │ ├── kg-integration.ts
│ │ ├── kg-link-validator.ts
│ │ ├── kg-storage.ts
│ │ ├── knowledge-graph.ts
│ │ ├── learning.ts
│ │ ├── manager.ts
│ │ ├── multi-agent-sharing.ts
│ │ ├── pruning.ts
│ │ ├── schemas.ts
│ │ ├── storage.ts
│ │ ├── temporal-analysis.ts
│ │ ├── user-preferences.ts
│ │ └── visualization.ts
│ ├── prompts
│ │ └── technical-writer-prompts.ts
│ ├── scripts
│ │ └── benchmark.ts
│ ├── templates
│ │ └── playwright
│ │ ├── accessibility.spec.template.ts
│ │ ├── Dockerfile.template
│ │ ├── docs-e2e.workflow.template.yml
│ │ ├── link-validation.spec.template.ts
│ │ └── playwright.config.template.ts
│ ├── tools
│ │ ├── analyze-deployments.ts
│ │ ├── analyze-readme.ts
│ │ ├── analyze-repository.ts
│ │ ├── check-documentation-links.ts
│ │ ├── deploy-pages.ts
│ │ ├── detect-gaps.ts
│ │ ├── evaluate-readme-health.ts
│ │ ├── generate-config.ts
│ │ ├── generate-contextual-content.ts
│ │ ├── generate-llm-context.ts
│ │ ├── generate-readme-template.ts
│ │ ├── generate-technical-writer-prompts.ts
│ │ ├── kg-health-check.ts
│ │ ├── manage-preferences.ts
│ │ ├── manage-sitemap.ts
│ │ ├── optimize-readme.ts
│ │ ├── populate-content.ts
│ │ ├── readme-best-practices.ts
│ │ ├── recommend-ssg.ts
│ │ ├── setup-playwright-tests.ts
│ │ ├── setup-structure.ts
│ │ ├── sync-code-to-docs.ts
│ │ ├── test-local-deployment.ts
│ │ ├── track-documentation-freshness.ts
│ │ ├── update-existing-documentation.ts
│ │ ├── validate-content.ts
│ │ ├── validate-documentation-freshness.ts
│ │ ├── validate-readme-checklist.ts
│ │ └── verify-deployment.ts
│ ├── types
│ │ └── api.ts
│ ├── utils
│ │ ├── ast-analyzer.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── permission-checker.ts
│ │ └── sitemap-generator.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── functional
│ │ └── tools.test.ts
│ ├── integration
│ │ ├── kg-documentation-workflow.test.ts
│ │ ├── knowledge-graph-workflow.test.ts
│ │ ├── mcp-readme-tools.test.ts
│ │ ├── memory-mcp-tools.test.ts
│ │ ├── readme-technical-writer.test.ts
│ │ └── workflow.test.ts
│ ├── memory
│ │ ├── contextual-retrieval.test.ts
│ │ ├── enhanced-manager.test.ts
│ │ ├── export-import.test.ts
│ │ ├── freshness-kg-integration.test.ts
│ │ ├── kg-code-integration.test.ts
│ │ ├── kg-health.test.ts
│ │ ├── kg-link-validator.test.ts
│ │ ├── kg-storage-validation.test.ts
│ │ ├── kg-storage.test.ts
│ │ ├── knowledge-graph-enhanced.test.ts
│ │ ├── knowledge-graph.test.ts
│ │ ├── learning.test.ts
│ │ ├── manager-advanced.test.ts
│ │ ├── manager.test.ts
│ │ ├── mcp-resource-integration.test.ts
│ │ ├── mcp-tool-persistence.test.ts
│ │ ├── schemas.test.ts
│ │ ├── storage.test.ts
│ │ ├── temporal-analysis.test.ts
│ │ └── user-preferences.test.ts
│ ├── performance
│ │ ├── memory-load-testing.test.ts
│ │ └── memory-stress-testing.test.ts
│ ├── prompts
│ │ ├── guided-workflow-prompts.test.ts
│ │ └── technical-writer-prompts.test.ts
│ ├── server.test.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── all-tools.test.ts
│ │ ├── analyze-coverage.test.ts
│ │ ├── analyze-deployments.test.ts
│ │ ├── analyze-readme.test.ts
│ │ ├── analyze-repository.test.ts
│ │ ├── check-documentation-links.test.ts
│ │ ├── deploy-pages-kg-retrieval.test.ts
│ │ ├── deploy-pages-tracking.test.ts
│ │ ├── deploy-pages.test.ts
│ │ ├── detect-gaps.test.ts
│ │ ├── evaluate-readme-health.test.ts
│ │ ├── generate-contextual-content.test.ts
│ │ ├── generate-llm-context.test.ts
│ │ ├── generate-readme-template.test.ts
│ │ ├── generate-technical-writer-prompts.test.ts
│ │ ├── kg-health-check.test.ts
│ │ ├── manage-sitemap.test.ts
│ │ ├── optimize-readme.test.ts
│ │ ├── readme-best-practices.test.ts
│ │ ├── recommend-ssg-historical.test.ts
│ │ ├── recommend-ssg-preferences.test.ts
│ │ ├── recommend-ssg.test.ts
│ │ ├── simple-coverage.test.ts
│ │ ├── sync-code-to-docs.test.ts
│ │ ├── test-local-deployment.test.ts
│ │ ├── tool-error-handling.test.ts
│ │ ├── track-documentation-freshness.test.ts
│ │ ├── validate-content.test.ts
│ │ ├── validate-documentation-freshness.test.ts
│ │ └── validate-readme-checklist.test.ts
│ ├── types
│ │ └── type-safety.test.ts
│ └── utils
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ └── sitemap-generator.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/src/memory/enhanced-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Enhanced Memory Manager with Learning and Knowledge Graph Integration
* Combines Issues #47 and #48 for intelligent memory management
*/
import { MemoryManager } from "./manager.js";
import { MemoryEntry } from "./storage.js";
import IncrementalLearningSystem, {
ProjectFeatures,
LearningInsight,
} from "./learning.js";
import KnowledgeGraph, { RecommendationPath } from "./knowledge-graph.js";
export interface EnhancedRecommendation {
baseRecommendation: any;
learningEnhanced: any;
graphBased: RecommendationPath[];
insights: LearningInsight[];
confidence: number;
reasoning: string[];
metadata: {
usedLearning: boolean;
usedKnowledgeGraph: boolean;
patternsFound: number;
similarProjects: number;
};
}
export interface IntelligentAnalysis {
analysis: any;
patterns: string[];
predictions: Array<{
type: "success_rate" | "optimal_ssg" | "potential_issues";
prediction: string;
confidence: number;
}>;
recommendations: string[];
learningData: {
similarProjects: number;
confidenceLevel: number;
dataQuality: "low" | "medium" | "high";
};
}
export class EnhancedMemoryManager extends MemoryManager {
private learningSystem: IncrementalLearningSystem;
private knowledgeGraph: KnowledgeGraph;
private initialized: boolean = false;
constructor(storageDir?: string) {
super(storageDir);
this.learningSystem = new IncrementalLearningSystem(this);
this.knowledgeGraph = new KnowledgeGraph(this);
}
async initialize(): Promise<void> {
await super.initialize();
if (!this.initialized) {
await this.learningSystem.initialize();
await this.knowledgeGraph.initialize();
this.initialized = true;
// Set up automatic learning from new memories
this.on("memory-created", this.handleNewMemory.bind(this));
}
}
/**
* Enhanced recommendation that combines base analysis with learning and graph intelligence
*/
async getEnhancedRecommendation(
projectPath: string,
baseRecommendation: any,
projectFeatures: ProjectFeatures,
): Promise<EnhancedRecommendation> {
await this.initialize();
// Get learning-enhanced recommendation
const learningResult = await this.learningSystem.getImprovedRecommendation(
projectFeatures,
baseRecommendation,
);
// Get knowledge graph-based recommendations
const candidateSSGs = this.extractCandidateSSGs(baseRecommendation);
const graphRecommendations =
await this.knowledgeGraph.getGraphBasedRecommendation(
projectFeatures,
candidateSSGs,
);
// Combine insights and reasoning
const allInsights = [...learningResult.insights];
const reasoning: string[] = [];
// Add graph-based reasoning
if (graphRecommendations.length > 0) {
const topRecommendation = graphRecommendations[0];
reasoning.push(...topRecommendation.reasoning);
allInsights.push({
type: "recommendation",
message: `Knowledge graph analysis suggests ${topRecommendation.to.label} based on similar successful projects`,
confidence: topRecommendation.confidence,
actionable: true,
data: { graphPath: topRecommendation.path },
});
}
// Calculate combined confidence
const combinedConfidence = this.calculateCombinedConfidence(
baseRecommendation.confidence,
learningResult.confidence,
graphRecommendations[0]?.confidence || 0,
);
// Determine final recommendation
const finalRecommendation = learningResult.recommendation;
if (
graphRecommendations.length > 0 &&
graphRecommendations[0].confidence > 0.8
) {
const graphChoice = graphRecommendations[0].to.label;
if (graphChoice !== finalRecommendation.recommended) {
finalRecommendation.graphSuggestion = graphChoice;
finalRecommendation.conflictDetected = true;
reasoning.push(
`Knowledge graph suggests ${graphChoice} while learning system suggests ${finalRecommendation.recommended}`,
);
}
}
return {
baseRecommendation,
learningEnhanced: learningResult.recommendation,
graphBased: graphRecommendations,
insights: allInsights,
confidence: combinedConfidence,
reasoning,
metadata: {
usedLearning: learningResult.insights.length > 0,
usedKnowledgeGraph: graphRecommendations.length > 0,
patternsFound: await this.countRelevantPatterns(projectFeatures),
similarProjects: graphRecommendations.length,
},
};
}
/**
* Enhanced analysis that provides intelligent insights
*/
async getIntelligentAnalysis(
projectPath: string,
baseAnalysis: any,
): Promise<IntelligentAnalysis> {
await this.initialize();
const projectFeatures = this.extractProjectFeatures(baseAnalysis);
// Find patterns from similar projects
const patterns = await this.findProjectPatterns(projectFeatures);
// Generate predictions
const predictions = await this.generatePredictions(
projectFeatures,
patterns,
);
// Generate recommendations
const recommendations = await this.generateIntelligentRecommendations(
projectFeatures,
patterns,
predictions,
);
// Assess learning data quality
const learningData = await this.assessLearningData(projectFeatures);
return {
analysis: baseAnalysis,
patterns: patterns.map((p) => p.description),
predictions,
recommendations,
learningData,
};
}
/**
* Learn from new memory entries automatically
*/
private async handleNewMemory(memory: MemoryEntry): Promise<void> {
try {
// Determine outcome for learning
const outcome = this.inferOutcome(memory);
if (outcome) {
await this.learningSystem.learn(memory, outcome);
}
// Update knowledge graph
await this.knowledgeGraph.buildFromMemories();
// Periodically save graph to persistent storage
if (Math.random() < 0.1) {
// 10% chance to save
await this.knowledgeGraph.saveToMemory();
}
} catch (error) {
console.error("Error in automatic learning:", error);
}
}
/**
* Extract project features from analysis data
*/
private extractProjectFeatures(analysis: any): ProjectFeatures {
return {
language: analysis.language?.primary || "unknown",
framework: analysis.framework?.name,
size: this.categorizeProjectSize(analysis.stats?.files || 0),
complexity: this.categorizeProjectComplexity(analysis),
hasTests: Boolean(analysis.testing?.hasTests),
hasCI: Boolean(analysis.ci?.hasCI),
hasDocs: Boolean(analysis.documentation?.exists),
teamSize: analysis.team?.size,
isOpenSource: Boolean(analysis.repository?.isPublic),
};
}
private categorizeProjectSize(
fileCount: number,
): "small" | "medium" | "large" {
if (fileCount < 50) return "small";
if (fileCount < 200) return "medium";
return "large";
}
private categorizeProjectComplexity(
analysis: any,
): "simple" | "moderate" | "complex" {
let complexity = 0;
if (analysis.dependencies?.count > 20) complexity++;
if (analysis.framework?.name) complexity++;
if (analysis.testing?.frameworks?.length > 1) complexity++;
if (analysis.ci?.workflows?.length > 2) complexity++;
if (analysis.architecture?.patterns?.length > 3) complexity++;
if (complexity <= 1) return "simple";
if (complexity <= 3) return "moderate";
return "complex";
}
/**
* Extract candidate SSGs from base recommendation
*/
private extractCandidateSSGs(baseRecommendation: any): string[] {
const candidates = [baseRecommendation.recommended];
if (baseRecommendation.alternatives) {
candidates.push(
...baseRecommendation.alternatives.map((alt: any) => alt.name || alt),
);
}
return [...new Set(candidates)].filter(Boolean);
}
/**
* Calculate combined confidence from multiple sources
*/
private calculateCombinedConfidence(
baseConfidence: number,
learningConfidence: number,
graphConfidence: number,
): number {
// Weighted average with emphasis on learning and graph data when available
const weights = {
base: 0.4,
learning: learningConfidence > 0 ? 0.4 : 0,
graph: graphConfidence > 0 ? 0.2 : 0,
};
// Redistribute weights if some sources are unavailable
const totalWeight = weights.base + weights.learning + weights.graph;
const normalizedWeights = {
base: weights.base / totalWeight,
learning: weights.learning / totalWeight,
graph: weights.graph / totalWeight,
};
return (
baseConfidence * normalizedWeights.base +
learningConfidence * normalizedWeights.learning +
graphConfidence * normalizedWeights.graph
);
}
/**
* Count patterns relevant to project features
*/
private async countRelevantPatterns(
features: ProjectFeatures,
): Promise<number> {
const tags = [features.language, features.framework, features.size].filter(
(tag): tag is string => Boolean(tag),
);
const memories = await this.search({
tags,
});
return memories.length;
}
/**
* Find patterns from similar projects
*/
private async findProjectPatterns(features: ProjectFeatures): Promise<
Array<{
type: string;
description: string;
confidence: number;
frequency: number;
}>
> {
const patterns: Array<{
type: string;
description: string;
confidence: number;
frequency: number;
}> = [];
// Find similar projects based on features
const similarMemories = await this.search({
tags: [features.language],
});
if (similarMemories.length >= 3) {
// Pattern: Most common SSG for this language
const ssgCounts = new Map<string, number>();
for (const memory of similarMemories) {
if (memory.metadata.ssg) {
ssgCounts.set(
memory.metadata.ssg,
(ssgCounts.get(memory.metadata.ssg) || 0) + 1,
);
}
}
if (ssgCounts.size > 0) {
const topSSG = Array.from(ssgCounts.entries()).sort(
([, a], [, b]) => b - a,
)[0];
patterns.push({
type: "ssg_preference",
description: `${topSSG[0]} is commonly used with ${features.language} (${topSSG[1]}/${similarMemories.length} projects)`,
confidence: topSSG[1] / similarMemories.length,
frequency: topSSG[1],
});
}
// Pattern: Success rate analysis
const deploymentMemories = similarMemories.filter(
(m) => m.type === "deployment",
);
if (deploymentMemories.length >= 2) {
const successRate =
deploymentMemories.filter((m) => m.data.status === "success").length /
deploymentMemories.length;
patterns.push({
type: "success_rate",
description: `Similar ${features.language} projects have ${(
successRate * 100
).toFixed(0)}% deployment success rate`,
confidence: Math.min(deploymentMemories.length / 10, 1.0),
frequency: deploymentMemories.length,
});
}
}
return patterns;
}
/**
* Generate predictions based on patterns and features
*/
private async generatePredictions(
features: ProjectFeatures,
patterns: Array<{ type: string; description: string; confidence: number }>,
): Promise<
Array<{
type: "success_rate" | "optimal_ssg" | "potential_issues";
prediction: string;
confidence: number;
}>
> {
const predictions: Array<{
type: "success_rate" | "optimal_ssg" | "potential_issues";
prediction: string;
confidence: number;
}> = [];
// Predict success rate
const successPattern = patterns.find((p) => p.type === "success_rate");
if (successPattern) {
predictions.push({
type: "success_rate",
prediction: `Expected deployment success rate: ${
successPattern.description.match(/(\d+)%/)?.[1] || "unknown"
}%`,
confidence: successPattern.confidence,
});
}
// Predict optimal SSG
const ssgPattern = patterns.find((p) => p.type === "ssg_preference");
if (ssgPattern) {
const ssg = ssgPattern.description.split(" ")[0];
predictions.push({
type: "optimal_ssg",
prediction: `${ssg} is likely the optimal choice based on similar projects`,
confidence: ssgPattern.confidence,
});
}
// Predict potential issues
if (!features.hasTests && features.size !== "small") {
predictions.push({
type: "potential_issues",
prediction:
"Deployment issues likely due to lack of tests in medium/large project",
confidence: 0.7,
});
}
if (!features.hasCI && features.complexity === "complex") {
predictions.push({
type: "potential_issues",
prediction:
"Complex project without CI/CD may face integration challenges",
confidence: 0.6,
});
}
return predictions;
}
/**
* Generate intelligent recommendations
*/
private async generateIntelligentRecommendations(
features: ProjectFeatures,
patterns: Array<{ type: string; description: string; confidence: number }>,
predictions: Array<{
type: string;
prediction: string;
confidence: number;
}>,
): Promise<string[]> {
const recommendations: string[] = [];
// Recommendations based on patterns
const ssgPattern = patterns.find((p) => p.type === "ssg_preference");
if (ssgPattern && ssgPattern.confidence > 0.7) {
const ssg = ssgPattern.description.split(" ")[0];
recommendations.push(
`Consider ${ssg} - it's proven successful for similar ${features.language} projects`,
);
}
// Recommendations based on predictions
const issuesPrediction = predictions.find(
(p) => p.type === "potential_issues",
);
if (issuesPrediction && issuesPrediction.confidence > 0.6) {
if (issuesPrediction.prediction.includes("tests")) {
recommendations.push(
"Set up automated testing before deployment to improve success rate",
);
}
if (issuesPrediction.prediction.includes("CI/CD")) {
recommendations.push(
"Implement CI/CD pipeline to handle project complexity",
);
}
}
// Recommendations based on features
if (features.complexity === "complex" && !features.hasDocs) {
recommendations.push(
"Invest in comprehensive documentation for this complex project",
);
}
if (features.isOpenSource && features.size === "large") {
recommendations.push(
"Consider community-friendly documentation tools for large open-source project",
);
}
return recommendations;
}
/**
* Assess quality of learning data
*/
private async assessLearningData(features: ProjectFeatures): Promise<{
similarProjects: number;
confidenceLevel: number;
dataQuality: "low" | "medium" | "high";
}> {
const tags = [features.language, features.framework].filter(
(tag): tag is string => Boolean(tag),
);
const similarMemories = await this.search({
tags,
});
const similarProjects = similarMemories.length;
let confidenceLevel = 0;
if (similarProjects >= 10) {
confidenceLevel = 0.9;
} else if (similarProjects >= 5) {
confidenceLevel = 0.7;
} else if (similarProjects >= 2) {
confidenceLevel = 0.5;
} else {
confidenceLevel = 0.2;
}
let dataQuality: "low" | "medium" | "high";
if (similarProjects >= 8 && confidenceLevel >= 0.8) {
dataQuality = "high";
} else if (similarProjects >= 3 && confidenceLevel >= 0.5) {
dataQuality = "medium";
} else {
dataQuality = "low";
}
return {
similarProjects,
confidenceLevel,
dataQuality,
};
}
/**
* Infer outcome from memory entry
*/
private inferOutcome(
memory: MemoryEntry,
): "success" | "failure" | "neutral" | null {
if (memory.type === "deployment") {
if (memory.data.status === "success") return "success";
if (memory.data.status === "failed") return "failure";
}
if (memory.type === "recommendation" && memory.data.feedback) {
const rating = memory.data.feedback.rating || memory.data.feedback.score;
if (rating > 3) return "success";
if (rating < 3) return "failure";
}
return "neutral";
}
/**
* Get comprehensive learning statistics
*/
async getLearningStatistics(): Promise<{
learning: any;
knowledgeGraph: any;
combined: {
totalMemories: number;
enhancedRecommendations: number;
accuracyImprovement: number;
systemMaturity: "nascent" | "developing" | "mature";
};
}> {
const learningStats = await this.learningSystem.getStatistics();
const graphStats = this.knowledgeGraph.getStatistics();
const totalMemories = (await this.search("")).length;
const graphStatsResult = await graphStats;
const enhancedRecommendations =
learningStats.totalPatterns + graphStatsResult.nodeCount;
// Estimate accuracy improvement based on data volume
let accuracyImprovement = 0;
if (totalMemories >= 50) {
accuracyImprovement = Math.min(0.3, totalMemories / 200);
}
// Determine system maturity
let systemMaturity: "nascent" | "developing" | "mature";
if (totalMemories >= 100 && learningStats.totalPatterns >= 20) {
systemMaturity = "mature";
} else if (totalMemories >= 20 && learningStats.totalPatterns >= 5) {
systemMaturity = "developing";
} else {
systemMaturity = "nascent";
}
return {
learning: learningStats,
knowledgeGraph: graphStats,
combined: {
totalMemories,
enhancedRecommendations,
accuracyImprovement,
systemMaturity,
},
};
}
/**
* Close and cleanup
*/
async close(): Promise<void> {
await this.knowledgeGraph.saveToMemory();
await super.close();
}
}
export default EnhancedMemoryManager;
```
--------------------------------------------------------------------------------
/tests/integration/memory-mcp-tools.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Memory MCP Tools Integration Tests
* Tests integration between memory system and MCP tools
* Part of Issue #56 - Memory MCP Tools Integration Tests
*/
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import {
initializeMemory,
rememberAnalysis,
rememberRecommendation,
rememberDeployment,
rememberConfiguration,
recallProjectHistory,
getProjectInsights,
getSimilarProjects,
getMemoryStatistics,
} from "../../src/memory/integration.js";
import { analyzeRepository } from "../../src/tools/analyze-repository.js";
import { recommendSSG } from "../../src/tools/recommend-ssg.js";
describe("Memory MCP Tools Integration", () => {
let tempDir: string;
let testProjectDir: string;
beforeEach(async () => {
// Create unique temp directory for each test
tempDir = path.join(
os.tmpdir(),
`memory-mcp-integration-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`,
);
await fs.mkdir(tempDir, { recursive: true });
// Create a mock project structure for testing
testProjectDir = path.join(tempDir, "test-project");
await createMockProject(testProjectDir);
});
afterEach(async () => {
// Cleanup temp directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
async function createMockProject(projectPath: string) {
await fs.mkdir(projectPath, { recursive: true });
// Create package.json
await fs.writeFile(
path.join(projectPath, "package.json"),
JSON.stringify(
{
name: "test-project",
version: "1.0.0",
dependencies: {
react: "^18.0.0",
typescript: "^5.0.0",
},
devDependencies: {
jest: "^29.0.0",
},
},
null,
2,
),
);
// Create README.md
await fs.writeFile(
path.join(projectPath, "README.md"),
`# Test Project
A test project for memory integration testing.
## Features
- TypeScript support
- React components
- Jest testing
`,
);
// Create src directory with TypeScript files
await fs.mkdir(path.join(projectPath, "src"));
await fs.writeFile(
path.join(projectPath, "src/index.ts"),
'export const hello = "world";',
);
await fs.writeFile(
path.join(projectPath, "src/component.tsx"),
'import React from "react"; export const Component = () => <div>Hello</div>;',
);
// Create tests directory
await fs.mkdir(path.join(projectPath, "__tests__"));
await fs.writeFile(
path.join(projectPath, "__tests__/index.test.ts"),
'test("hello world", () => { expect(true).toBe(true); });',
);
// Create docs directory
await fs.mkdir(path.join(projectPath, "docs"));
await fs.writeFile(
path.join(projectPath, "docs/setup.md"),
"# Setup Guide\n\nHow to set up the project.",
);
}
describe("Memory Integration Initialization", () => {
test("should initialize memory system for MCP tools", async () => {
const memoryManager = await initializeMemory();
expect(memoryManager).toBeDefined();
expect(memoryManager.constructor.name).toBe("MemoryManager");
});
test("should handle memory system events", async () => {
const memoryManager = await initializeMemory();
let eventsFired = 0;
memoryManager.on("memory-created", () => {
eventsFired++;
});
// Create a memory entry
await memoryManager.remember("analysis", { test: true });
// Give events time to fire
await new Promise((resolve) => setTimeout(resolve, 50));
expect(eventsFired).toBeGreaterThanOrEqual(1);
});
});
describe("Repository Analysis Integration", () => {
test("should integrate repository analysis with memory system", async () => {
// Run repository analysis tool
const analysisResult = await analyzeRepository({
path: testProjectDir,
depth: "standard",
});
expect(analysisResult.isError).toBeFalsy();
expect(analysisResult.content).toBeDefined();
// Extract analysis data from MCP response
const analysisContent = analysisResult.content.find(
(c) => c.type === "text" && c.text.includes("Analysis Complete"),
);
expect(analysisContent).toBeDefined();
// Remember analysis in memory system
const analysisData = {
projectId: "test-project",
language: { primary: "typescript" },
framework: { name: "react" },
stats: { files: 5 },
repository: { url: "github.com/test/project" },
};
const memoryId = await rememberAnalysis(testProjectDir, analysisData);
expect(memoryId).toBeDefined();
expect(typeof memoryId).toBe("string");
// Verify memory was stored
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled).not.toBeNull();
expect(recalled?.data.language.primary).toBe("typescript");
expect(recalled?.data.framework.name).toBe("react");
});
test("should store analysis metadata correctly", async () => {
const analysisData = {
projectId: "metadata-test",
language: { primary: "javascript" },
framework: { name: "vue" },
stats: { files: 25 },
repository: { url: "github.com/test/vue-project" },
};
const memoryId = await rememberAnalysis(
"/test/vue-project",
analysisData,
);
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.metadata.repository).toBe("github.com/test/vue-project");
expect(recalled?.metadata.tags).toContain("analysis");
expect(recalled?.metadata.tags).toContain("javascript");
expect(recalled?.metadata.tags).toContain("vue");
});
});
describe("SSG Recommendation Integration", () => {
test("should integrate SSG recommendation with memory system", async () => {
// First create an analysis
const analysisData = {
projectId: "ssg-test",
language: { primary: "typescript" },
framework: { name: "react" },
};
const analysisId = await rememberAnalysis(
"/test/ssg-project",
analysisData,
);
// Run SSG recommendation tool
const recommendationResult = await recommendSSG({
analysisId,
preferences: {
priority: "features",
ecosystem: "javascript",
},
});
expect(recommendationResult.content).toBeDefined();
// Extract recommendation data
const recommendationData = {
recommended: "docusaurus",
confidence: 0.85,
reasoning: ["React-based", "TypeScript support"],
analysisId,
};
const memoryId = await rememberRecommendation(
analysisId,
recommendationData,
);
expect(memoryId).toBeDefined();
// Verify memory linking
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.data.recommended).toBe("docusaurus");
expect(recalled?.metadata.ssg).toBe("docusaurus");
expect(recalled?.metadata.tags).toContain("recommendation");
expect(recalled?.metadata.tags).toContain("docusaurus");
});
test("should link recommendations to analysis", async () => {
// Create analysis
const analysisData = {
projectId: "linked-test",
language: { primary: "python" },
};
const analysisId = await rememberAnalysis(
"/test/python-project",
analysisData,
);
// Create recommendation
const recommendationData = {
recommended: "mkdocs",
confidence: 0.9,
};
const recommendationId = await rememberRecommendation(
analysisId,
recommendationData,
);
// Verify linking
const memoryManager = await initializeMemory();
const recommendation = await memoryManager.recall(recommendationId);
const analysis = await memoryManager.recall(analysisId);
expect(recommendation?.metadata.projectId).toBe(
analysis?.metadata.projectId,
);
expect(recommendation?.metadata.projectId).toBe("linked-test");
});
});
describe("Deployment Memory Integration", () => {
test("should store deployment results in memory", async () => {
const deploymentData = {
ssg: "hugo",
status: "success",
duration: 120,
url: "https://test-project.github.io",
branch: "gh-pages",
};
const memoryId = await rememberDeployment(
"github.com/test/project",
deploymentData,
);
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.data.ssg).toBe("hugo");
expect(recalled?.data.status).toBe("success");
expect(recalled?.metadata.repository).toBe("github.com/test/project");
expect(recalled?.metadata.tags).toContain("deployment");
expect(recalled?.metadata.tags).toContain("success");
expect(recalled?.metadata.tags).toContain("hugo");
});
test("should track deployment failures", async () => {
const failedDeployment = {
ssg: "jekyll",
status: "failed",
error: "Build failed: missing dependency",
duration: 45,
};
const memoryId = await rememberDeployment(
"github.com/test/failed-project",
failedDeployment,
);
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.data.status).toBe("failed");
expect(recalled?.data.error).toContain("Build failed");
expect(recalled?.metadata.tags).toContain("failed");
});
});
describe("Configuration Memory Integration", () => {
test("should store configuration data in memory", async () => {
const configData = {
title: "Test Documentation",
theme: "material",
plugins: ["search", "navigation"],
build: {
outputDir: "_site",
baseUrl: "/docs/",
},
};
const memoryId = await rememberConfiguration(
"test-docs",
"mkdocs",
configData,
);
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.data.title).toBe("Test Documentation");
expect(recalled?.data.theme).toBe("material");
expect(recalled?.metadata.ssg).toBe("mkdocs");
expect(recalled?.metadata.tags).toContain("configuration");
expect(recalled?.metadata.tags).toContain("mkdocs");
expect(recalled?.metadata.tags).toContain("test-docs");
});
});
describe("Project History and Insights", () => {
test("should recall comprehensive project history", async () => {
const projectId = "history-test";
// Create a complete project workflow in memory
const analysisData = {
projectId,
language: { primary: "typescript" },
framework: { name: "react" },
};
await rememberAnalysis("/test/history-project", analysisData);
const recommendationData = {
recommended: "docusaurus",
confidence: 0.8,
};
await rememberRecommendation("analysis-id", recommendationData);
const deploymentData = {
ssg: "docusaurus",
status: "success",
duration: 90,
};
await rememberDeployment(
"github.com/test/history-project",
deploymentData,
);
// Recall project history
const history = await recallProjectHistory(projectId);
expect(history.projectId).toBe(projectId);
expect(history.history).toBeDefined();
expect(history.insights).toBeDefined();
expect(Array.isArray(history.insights)).toBe(true);
});
test("should generate meaningful project insights", async () => {
const projectId = "insights-test";
// Create deployment history
const successfulDeployment = {
ssg: "hugo",
status: "success",
duration: 60,
};
await rememberDeployment(
"github.com/test/insights-project",
successfulDeployment,
);
const failedDeployment = {
ssg: "hugo",
status: "failed",
error: "Build timeout",
};
await rememberDeployment(
"github.com/test/insights-project",
failedDeployment,
);
const insights = await getProjectInsights(projectId);
expect(Array.isArray(insights)).toBe(true);
// Insights may be empty if memories don't meet criteria, that's ok
if (insights.length > 0) {
// Should include deployment success rate if deployments exist
const successRateInsight = insights.find((i) =>
i.includes("success rate"),
);
expect(successRateInsight).toBeDefined();
}
});
});
describe("Similar Projects Discovery", () => {
test("should find similar projects based on characteristics", async () => {
// Create multiple projects with different characteristics
await rememberAnalysis("/project1", {
projectId: "project1",
language: { primary: "typescript" },
framework: { name: "react" },
});
await rememberAnalysis("/project2", {
projectId: "project2",
language: { primary: "typescript" },
framework: { name: "vue" },
});
await rememberAnalysis("/project3", {
projectId: "project3",
language: { primary: "python" },
framework: { name: "django" },
});
// Search for similar projects
const targetProject = {
language: { primary: "typescript" },
framework: { name: "react" },
stats: { files: 100 },
};
const similarProjects = await getSimilarProjects(targetProject, 3);
expect(Array.isArray(similarProjects)).toBe(true);
// Similar projects may be empty if no matches found, that's ok
if (similarProjects.length > 0) {
// Should find TypeScript projects first
const tsProjects = similarProjects.filter((p) => p.similarity > 0);
expect(tsProjects.length).toBeGreaterThan(0);
}
});
test("should calculate similarity scores correctly", async () => {
// Create projects with known characteristics
await rememberAnalysis("/exact-match", {
projectId: "exact-match",
language: { primary: "javascript" },
framework: { name: "vue" },
stats: { files: 50 },
documentation: { type: "api" },
});
await rememberAnalysis("/partial-match", {
projectId: "partial-match",
language: { primary: "javascript" },
framework: { name: "react" },
stats: { files: 45 },
});
const targetProject = {
language: { primary: "javascript" },
framework: { name: "vue" },
stats: { files: 52 },
documentation: { type: "api" },
};
const similarProjects = await getSimilarProjects(targetProject, 5);
expect(Array.isArray(similarProjects)).toBe(true);
// Similar projects may be empty, but if found should have similarity scores
if (similarProjects.length > 0) {
const exactMatch = similarProjects.find(
(p) => p.projectId === "exact-match",
);
const partialMatch = similarProjects.find(
(p) => p.projectId === "partial-match",
);
if (exactMatch && partialMatch) {
expect(exactMatch.similarity).toBeGreaterThan(
partialMatch.similarity,
);
}
}
});
});
describe("Memory Statistics and Analytics", () => {
test("should provide memory statistics for tools", async () => {
// Create some test data
await rememberAnalysis("/stats-test", {
projectId: "stats-test",
language: { primary: "go" },
});
await rememberDeployment("github.com/test/stats", {
ssg: "hugo",
status: "success",
});
const stats = await getMemoryStatistics();
expect(stats).toBeDefined();
expect(typeof stats).toBe("object");
});
});
describe("Error Handling and Edge Cases", () => {
test("should handle malformed analysis data gracefully", async () => {
const malformedData = {
// Missing required fields
language: null,
framework: undefined,
};
// Should not throw but handle gracefully
const memoryId = await rememberAnalysis("/malformed", malformedData);
expect(memoryId).toBeDefined();
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled).not.toBeNull();
});
test("should handle missing analysis when creating recommendations", async () => {
const recommendationData = {
recommended: "jekyll",
confidence: 0.7,
};
// Reference non-existent analysis
const memoryId = await rememberRecommendation(
"non-existent-analysis",
recommendationData,
);
expect(memoryId).toBeDefined();
const memoryManager = await initializeMemory();
const recalled = await memoryManager.recall(memoryId);
expect(recalled?.data.recommended).toBe("jekyll");
});
test("should handle empty project history gracefully", async () => {
const history = await recallProjectHistory("non-existent-project");
expect(history.projectId).toBe("non-existent-project");
expect(history.history).toBeDefined();
expect(Array.isArray(history.insights)).toBe(true);
});
test("should handle similar projects search with no matches", async () => {
const uniqueProject = {
language: { primary: "rust" },
framework: { name: "actix" },
stats: { files: 500 },
};
const similarProjects = await getSimilarProjects(uniqueProject, 5);
expect(Array.isArray(similarProjects)).toBe(true);
// Should return empty array or minimal matches
expect(similarProjects.length).toBeGreaterThanOrEqual(0);
});
});
});
```
--------------------------------------------------------------------------------
/tests/utils/content-extractor.test.ts:
--------------------------------------------------------------------------------
```typescript
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import { extractRepositoryContent } from "../../src/utils/content-extractor";
describe("Content Extractor", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = path.join(os.tmpdir(), `content-extractor-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
});
afterEach(async () => {
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe("extractRepositoryContent", () => {
it("should extract README.md content", async () => {
const readmeContent = `# Test Project\n## Installation\nRun npm install\n## Usage\nUse it like this`;
await fs.writeFile(path.join(tempDir, "README.md"), readmeContent);
const result = await extractRepositoryContent(tempDir);
expect(result.readme).toBeDefined();
expect(result.readme?.content).toBe(readmeContent);
expect(result.readme?.sections.length).toBeGreaterThan(0);
expect(result.readme?.sections[0].title).toBe("Test Project");
});
it("should extract readme.md (lowercase) content", async () => {
const readmeContent = `# Test\nContent`;
await fs.writeFile(path.join(tempDir, "readme.md"), readmeContent);
const result = await extractRepositoryContent(tempDir);
expect(result.readme).toBeDefined();
expect(result.readme?.content).toBe(readmeContent);
});
it("should extract Readme.md (mixed case) content", async () => {
const readmeContent = `# Test\nContent`;
await fs.writeFile(path.join(tempDir, "Readme.md"), readmeContent);
const result = await extractRepositoryContent(tempDir);
expect(result.readme).toBeDefined();
expect(result.readme?.content).toBe(readmeContent);
});
it("should return undefined when no README exists", async () => {
const result = await extractRepositoryContent(tempDir);
expect(result.readme).toBeUndefined();
});
it("should extract existing documentation from docs directory", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "guide.md"),
"# Guide\nHow to do things",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBeGreaterThan(0);
expect(result.existingDocs[0].title).toBe("Guide");
expect(result.existingDocs[0].path).toContain("guide.md");
});
it("should extract documentation from documentation directory", async () => {
const docsDir = path.join(tempDir, "documentation");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(path.join(docsDir, "test.md"), "# Test Doc\nContent");
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBeGreaterThan(0);
});
it("should extract documentation from doc directory", async () => {
const docsDir = path.join(tempDir, "doc");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(path.join(docsDir, "test.md"), "# Test\nContent");
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBeGreaterThan(0);
});
it("should extract .mdx files", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "component.mdx"),
"# Component\nJSX content",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBeGreaterThan(0);
expect(result.existingDocs[0].path).toContain("component.mdx");
});
it("should recursively extract docs from subdirectories", async () => {
const docsDir = path.join(tempDir, "docs");
const subDir = path.join(docsDir, "guides");
await fs.mkdir(subDir, { recursive: true });
await fs.writeFile(
path.join(subDir, "tutorial.md"),
"# Tutorial\nStep by step",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBeGreaterThan(0);
expect(result.existingDocs[0].path).toContain("guides");
});
it("should skip hidden directories", async () => {
const docsDir = path.join(tempDir, "docs");
const hiddenDir = path.join(docsDir, ".hidden");
await fs.mkdir(hiddenDir, { recursive: true });
await fs.writeFile(
path.join(hiddenDir, "secret.md"),
"# Secret\nContent",
);
const result = await extractRepositoryContent(tempDir);
expect(
result.existingDocs.find((d) => d.path.includes(".hidden")),
).toBeUndefined();
});
it("should categorize documents as tutorial", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "getting-started.md"),
"# Getting Started\n## Step 1\nFirst, do this",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs[0].category).toBe("tutorial");
});
it("should categorize documents as how-to", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "how-to-deploy.md"),
"# How to Deploy\nYou can deploy by...",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs[0].category).toBe("how-to");
});
it("should categorize documents as reference", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "api.md"),
"# API Reference\nEndpoints",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs[0].category).toBe("reference");
});
it("should categorize documents as explanation", async () => {
const docsDir = path.join(tempDir, "docs");
const adrDir = path.join(docsDir, "adrs");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "001-decision.md"),
"# Decision\nExplanation",
);
const result = await extractRepositoryContent(tempDir);
const adrDocs = result.existingDocs.filter((d) => d.path.includes("adr"));
expect(adrDocs.length).toBeGreaterThan(0);
expect(adrDocs[0].category).toBe("explanation");
});
it("should extract title from first heading", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(
path.join(docsDir, "doc.md"),
"Some text\n# Main Title\nContent",
);
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs[0].title).toBe("Main Title");
});
it("should use filename as title when no heading exists", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(path.join(docsDir, "my-doc-file.md"), "No heading");
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs[0].title).toBe("my doc file");
});
it("should extract ADRs from docs/adrs", async () => {
const adrDir = path.join(tempDir, "docs/adrs");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "001-use-typescript.md"),
"# 1. Use TypeScript\n## Status\nAccepted\n## Decision\nWe will use TypeScript\n## Consequences\nBetter type safety",
);
const result = await extractRepositoryContent(tempDir);
expect(result.adrs.length).toBeGreaterThan(0);
expect(result.adrs[0].number).toBe("001");
expect(result.adrs[0].title).toBe("Use TypeScript");
expect(result.adrs[0].status).toBe("Accepted");
expect(result.adrs[0].decision).toContain("TypeScript");
expect(result.adrs[0].consequences).toContain("type safety");
});
it("should extract ADRs from docs/adr", async () => {
const adrDir = path.join(tempDir, "docs/adr");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "0001-test.md"),
"# Test\n## Status\nDraft\n## Decision\nTest",
);
const result = await extractRepositoryContent(tempDir);
expect(result.adrs.length).toBeGreaterThan(0);
});
it("should extract ADRs from docs/decisions", async () => {
const adrDir = path.join(tempDir, "docs/decisions");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "0001-test.md"),
"# Test\n## Status\nDraft\n## Decision\nTest",
);
const result = await extractRepositoryContent(tempDir);
expect(result.adrs.length).toBeGreaterThan(0);
});
it("should skip ADRs without number in filename", async () => {
const adrDir = path.join(tempDir, "docs/adrs");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "template.md"),
"# Template\n## Status\nN/A",
);
const result = await extractRepositoryContent(tempDir);
expect(result.adrs.length).toBe(0);
});
it("should extract code examples from examples directory", async () => {
const examplesDir = path.join(tempDir, "examples");
await fs.mkdir(examplesDir, { recursive: true });
await fs.writeFile(
path.join(examplesDir, "hello.js"),
"// Example: Hello World\nconsole.log('hello');",
);
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples.length).toBeGreaterThan(0);
expect(result.codeExamples[0].language).toBe("javascript");
expect(result.codeExamples[0].file).toBe("hello.js");
});
it("should extract code examples from samples directory", async () => {
const samplesDir = path.join(tempDir, "samples");
await fs.mkdir(samplesDir, { recursive: true });
await fs.writeFile(
path.join(samplesDir, "test.py"),
"# Demo: Python example\nprint('test')",
);
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples.length).toBeGreaterThan(0);
expect(result.codeExamples[0].language).toBe("python");
});
it("should extract code examples from demo directory", async () => {
const demoDir = path.join(tempDir, "demo");
await fs.mkdir(demoDir, { recursive: true });
await fs.writeFile(path.join(demoDir, "test.ts"), "const x = 1;");
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples.length).toBeGreaterThan(0);
expect(result.codeExamples[0].language).toBe("typescript");
});
it("should extract inline examples from @example tags", async () => {
const srcDir = path.join(tempDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "utils.ts"),
"/**\n * @example\n * const result = add(1, 2);\n */\nfunction add(a, b) { return a + b; }",
);
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples.length).toBeGreaterThan(0);
expect(result.codeExamples[0].code).toContain("add(1, 2)");
});
it("should support various programming languages", async () => {
const examplesDir = path.join(tempDir, "examples");
await fs.mkdir(examplesDir, { recursive: true });
await fs.writeFile(path.join(examplesDir, "test.rb"), "puts 'hello'");
await fs.writeFile(path.join(examplesDir, "test.go"), "package main");
await fs.writeFile(path.join(examplesDir, "test.java"), "class Test {}");
await fs.writeFile(path.join(examplesDir, "test.rs"), "fn main() {}");
const result = await extractRepositoryContent(tempDir);
const languages = result.codeExamples.map((e) => e.language);
expect(languages).toContain("ruby");
expect(languages).toContain("go");
expect(languages).toContain("java");
expect(languages).toContain("rust");
});
it("should extract API docs from markdown files", async () => {
const apiContent = `## \`getUserById\` function\n\nGet user by ID\n\n### Parameters\n\n- \`id\` (string) - User ID\n\n### Returns\n\nUser object`;
await fs.writeFile(path.join(tempDir, "api.md"), apiContent);
const result = await extractRepositoryContent(tempDir);
expect(result.apiDocs.length).toBeGreaterThan(0);
expect(result.apiDocs[0].function).toBe("getUserById");
expect(result.apiDocs[0].parameters.length).toBeGreaterThan(0);
});
it("should extract API docs from OpenAPI spec", async () => {
const openApiSpec = {
paths: {
"/users": {
get: {
summary: "List users",
parameters: [
{
name: "page",
type: "integer",
description: "Page number",
},
],
responses: {
"200": {
description: "Success",
},
},
},
},
},
};
await fs.writeFile(
path.join(tempDir, "openapi.json"),
JSON.stringify(openApiSpec),
);
const result = await extractRepositoryContent(tempDir);
expect(result.apiDocs.length).toBeGreaterThan(0);
expect(result.apiDocs[0].endpoint).toContain("GET");
});
it("should extract JSDoc from source files", async () => {
const srcDir = path.join(tempDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "utils.js"),
"/**\n * Add two numbers\n * @param {number} a - First number\n * @param {number} b - Second number\n * @returns {number} Sum of a and b\n */\nfunction add(a, b) { return a + b; }",
);
const result = await extractRepositoryContent(tempDir);
// JSDoc extraction may or may not find the function depending on parsing
// Just ensure it doesn't crash and returns valid structure
expect(result.apiDocs).toBeDefined();
expect(Array.isArray(result.apiDocs)).toBe(true);
});
it("should handle empty repository gracefully", async () => {
const result = await extractRepositoryContent(tempDir);
expect(result.readme).toBeUndefined();
expect(result.existingDocs).toEqual([]);
expect(result.adrs).toEqual([]);
expect(result.codeExamples).toEqual([]);
expect(result.apiDocs).toEqual([]);
});
it("should handle unreadable files gracefully", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
// Create a file and immediately make it unreadable (if possible)
const filePath = path.join(docsDir, "test.md");
await fs.writeFile(filePath, "content");
// The function should handle errors gracefully and continue
const result = await extractRepositoryContent(tempDir);
expect(result).toBeDefined();
});
it("should parse markdown sections correctly", async () => {
const readmeContent = `# Main\nIntro\n## Section 1\nContent 1\n### Subsection\nContent 2\n## Section 2\nContent 3`;
await fs.writeFile(path.join(tempDir, "README.md"), readmeContent);
const result = await extractRepositoryContent(tempDir);
expect(result.readme?.sections.length).toBeGreaterThan(2);
expect(result.readme?.sections[0].level).toBe(1);
expect(result.readme?.sections[1].level).toBe(2);
});
it("should handle ADRs with 4-digit numbers", async () => {
const adrDir = path.join(tempDir, "docs/adrs");
await fs.mkdir(adrDir, { recursive: true });
await fs.writeFile(
path.join(adrDir, "1234-long-number.md"),
"# Test\n## Status\nAccepted\n## Decision\nTest",
);
const result = await extractRepositoryContent(tempDir);
expect(result.adrs.length).toBeGreaterThan(0);
expect(result.adrs[0].number).toBe("1234");
});
it("should extract example description from code comments", async () => {
const examplesDir = path.join(tempDir, "examples");
await fs.mkdir(examplesDir, { recursive: true });
await fs.writeFile(
path.join(examplesDir, "test.js"),
"// Example: This is a demo\nconsole.log('test');",
);
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples[0].description).toContain("Example:");
});
it("should limit code example length", async () => {
const examplesDir = path.join(tempDir, "examples");
await fs.mkdir(examplesDir, { recursive: true });
const longCode = "x".repeat(1000);
await fs.writeFile(path.join(examplesDir, "test.js"), longCode);
const result = await extractRepositoryContent(tempDir);
expect(result.codeExamples[0].code.length).toBeLessThanOrEqual(500);
});
it("should handle invalid OpenAPI spec gracefully", async () => {
await fs.writeFile(path.join(tempDir, "openapi.json"), "invalid json{");
const result = await extractRepositoryContent(tempDir);
// Should not crash, just skip the invalid spec
expect(result).toBeDefined();
});
it("should skip non-markdown files in docs", async () => {
const docsDir = path.join(tempDir, "docs");
await fs.mkdir(docsDir, { recursive: true });
await fs.writeFile(path.join(docsDir, "image.png"), "binary data");
await fs.writeFile(path.join(docsDir, "valid.md"), "# Valid\nContent");
const result = await extractRepositoryContent(tempDir);
expect(result.existingDocs.length).toBe(1);
expect(result.existingDocs[0].path).toContain("valid.md");
});
it("should handle swagger.yaml files", async () => {
await fs.writeFile(path.join(tempDir, "swagger.yaml"), "openapi: 3.0.0");
const result = await extractRepositoryContent(tempDir);
// Should attempt to parse it (even if it fails due to YAML parsing)
expect(result).toBeDefined();
});
});
});
```
--------------------------------------------------------------------------------
/src/memory/learning.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Incremental Learning System for DocuMCP
* Implements Issue #47: Incremental Learning System
*
* Enables continuous improvement of recommendations based on historical patterns,
* success rates, and user feedback to optimize SSG suggestions and documentation strategies.
*/
import { MemoryManager } from "./manager.js";
import { MemoryEntry } from "./storage.js";
export interface LearningPattern {
id: string;
type:
| "ssg_preference"
| "deployment_success"
| "project_similarity"
| "user_behavior";
pattern: Record<string, any>;
confidence: number;
sampleSize: number;
lastUpdated: string;
metadata: {
projectTypes?: string[];
technologies?: string[];
successRate?: number;
frequency?: number;
};
}
export interface LearningInsight {
type: "recommendation" | "warning" | "optimization";
message: string;
confidence: number;
actionable: boolean;
data: Record<string, any>;
}
export interface ProjectFeatures {
language: string;
framework?: string;
size: "small" | "medium" | "large";
complexity: "simple" | "moderate" | "complex";
hasTests: boolean;
hasCI: boolean;
hasDocs: boolean;
teamSize?: number;
isOpenSource: boolean;
}
export class IncrementalLearningSystem {
private memoryManager: MemoryManager;
private patterns: Map<string, LearningPattern>;
private learningEnabled: boolean = true;
private readonly minSampleSize = 3;
private readonly confidenceThreshold = 0.7;
constructor(memoryManager: MemoryManager) {
this.memoryManager = memoryManager;
this.patterns = new Map();
}
async initialize(): Promise<void> {
await this.loadPatterns();
await this.updatePatterns();
}
/**
* Learn from a new interaction result
*/
async learn(
interaction: MemoryEntry,
outcome: "success" | "failure" | "neutral",
feedback?: Record<string, any>,
): Promise<void> {
if (!this.learningEnabled) return;
const features = this.extractFeatures(interaction);
// Update SSG preference patterns
if (interaction.type === "recommendation" && interaction.metadata.ssg) {
await this.updateSSGPattern(features, interaction.metadata.ssg, outcome);
}
// Update deployment success patterns
if (interaction.type === "deployment") {
await this.updateDeploymentPattern(features, outcome);
}
// Update project similarity patterns
if (interaction.type === "analysis") {
await this.updateSimilarityPattern(features, interaction);
}
// Learn from user feedback
if (feedback) {
await this.updateUserBehaviorPattern(features, feedback);
}
await this.persistPatterns();
}
/**
* Get improved recommendations based on learned patterns
*/
async getImprovedRecommendation(
projectFeatures: ProjectFeatures,
baseRecommendation: any,
): Promise<{
recommendation: any;
confidence: number;
insights: LearningInsight[];
}> {
const insights: LearningInsight[] = [];
const adjustedRecommendation = { ...baseRecommendation };
let confidenceBoost = 0;
// Apply SSG preference patterns
const ssgPattern = await this.getSSGPreferencePattern(projectFeatures);
if (ssgPattern && ssgPattern.confidence > this.confidenceThreshold) {
const preferredSSG = ssgPattern.pattern.preferredSSG;
if (preferredSSG !== baseRecommendation.recommended) {
insights.push({
type: "recommendation",
message: `Based on ${
ssgPattern.sampleSize
} similar projects, ${preferredSSG} has a ${(
ssgPattern.pattern.successRate * 100
).toFixed(0)}% success rate`,
confidence: ssgPattern.confidence,
actionable: true,
data: { suggestedSSG: preferredSSG, pattern: ssgPattern },
});
adjustedRecommendation.recommended = preferredSSG;
adjustedRecommendation.learningAdjusted = true;
confidenceBoost += 0.2;
}
}
// Apply deployment success patterns
const deploymentPattern = await this.getDeploymentPattern(projectFeatures);
if (
deploymentPattern &&
deploymentPattern.confidence > this.confidenceThreshold
) {
const riskFactors = deploymentPattern.pattern.riskFactors || [];
if (riskFactors.length > 0) {
insights.push({
type: "warning",
message: `Projects with similar characteristics have ${riskFactors.length} common deployment issues`,
confidence: deploymentPattern.confidence,
actionable: true,
data: { riskFactors, pattern: deploymentPattern },
});
adjustedRecommendation.deploymentWarnings = riskFactors;
}
}
// Apply optimization patterns
const optimizations = await this.getOptimizationInsights(projectFeatures);
insights.push(...optimizations);
const finalConfidence = Math.min(
baseRecommendation.confidence + confidenceBoost,
1.0,
);
return {
recommendation: adjustedRecommendation,
confidence: finalConfidence,
insights,
};
}
/**
* Extract features from project data for pattern matching
*/
private extractFeatures(interaction: MemoryEntry): ProjectFeatures {
const data = interaction.data;
return {
language: data.language?.primary || "unknown",
framework: data.framework?.name,
size: this.categorizeSize(data.stats?.files || 0),
complexity: this.categorizeComplexity(data),
hasTests: Boolean(data.testing?.hasTests),
hasCI: Boolean(data.ci?.hasCI),
hasDocs: Boolean(data.documentation?.exists),
isOpenSource: Boolean(data.repository?.isPublic),
};
}
private categorizeSize(fileCount: number): "small" | "medium" | "large" {
if (fileCount < 50) return "small";
if (fileCount < 200) return "medium";
return "large";
}
private categorizeComplexity(data: any): "simple" | "moderate" | "complex" {
let complexity = 0;
if (data.dependencies?.count > 20) complexity++;
if (data.framework?.name) complexity++;
if (data.testing?.frameworks?.length > 1) complexity++;
if (data.ci?.workflows?.length > 2) complexity++;
if (data.architecture?.patterns?.length > 3) complexity++;
if (complexity <= 1) return "simple";
if (complexity <= 3) return "moderate";
return "complex";
}
/**
* Update SSG preference patterns based on outcomes
*/
private async updateSSGPattern(
features: ProjectFeatures,
ssg: string,
outcome: "success" | "failure" | "neutral",
): Promise<void> {
const patternKey = this.generatePatternKey("ssg_preference", features);
const existing = this.patterns.get(patternKey);
if (existing) {
// Update existing pattern
const totalCount = existing.sampleSize;
const successCount = existing.pattern.successCount || 0;
const newSuccessCount =
outcome === "success" ? successCount + 1 : successCount;
existing.pattern.preferredSSG = ssg;
existing.pattern.successCount = newSuccessCount;
existing.pattern.successRate = newSuccessCount / (totalCount + 1);
existing.sampleSize = totalCount + 1;
existing.confidence = Math.min(existing.sampleSize / 10, 1.0);
existing.lastUpdated = new Date().toISOString();
} else {
// Create new pattern
const pattern: LearningPattern = {
id: patternKey,
type: "ssg_preference",
pattern: {
preferredSSG: ssg,
successCount: outcome === "success" ? 1 : 0,
successRate: outcome === "success" ? 1.0 : 0.0,
},
confidence: 0.1,
sampleSize: 1,
lastUpdated: new Date().toISOString(),
metadata: {
projectTypes: [features.language],
technologies: features.framework ? [features.framework] : [],
},
};
this.patterns.set(patternKey, pattern);
}
}
/**
* Update deployment success patterns
*/
private async updateDeploymentPattern(
features: ProjectFeatures,
outcome: "success" | "failure" | "neutral",
): Promise<void> {
const patternKey = this.generatePatternKey("deployment_success", features);
const existing = this.patterns.get(patternKey);
const riskFactors: string[] = [];
if (!features.hasTests) riskFactors.push("no_tests");
if (!features.hasCI) riskFactors.push("no_ci");
if (features.complexity === "complex") riskFactors.push("high_complexity");
if (features.size === "large") riskFactors.push("large_codebase");
if (existing) {
const totalCount = existing.sampleSize;
const successCount = existing.pattern.successCount || 0;
const newSuccessCount =
outcome === "success" ? successCount + 1 : successCount;
existing.pattern.successCount = newSuccessCount;
existing.pattern.successRate = newSuccessCount / (totalCount + 1);
existing.pattern.riskFactors = riskFactors;
existing.sampleSize = totalCount + 1;
existing.confidence = Math.min(existing.sampleSize / 10, 1.0);
existing.lastUpdated = new Date().toISOString();
} else {
const pattern: LearningPattern = {
id: patternKey,
type: "deployment_success",
pattern: {
successCount: outcome === "success" ? 1 : 0,
successRate: outcome === "success" ? 1.0 : 0.0,
riskFactors,
},
confidence: 0.1,
sampleSize: 1,
lastUpdated: new Date().toISOString(),
metadata: {
projectTypes: [features.language],
successRate: outcome === "success" ? 1.0 : 0.0,
},
};
this.patterns.set(patternKey, pattern);
}
}
/**
* Update project similarity patterns for better matching
*/
private async updateSimilarityPattern(
features: ProjectFeatures,
_interaction: MemoryEntry,
): Promise<void> {
const patternKey = this.generatePatternKey("project_similarity", features);
const existing = this.patterns.get(patternKey);
const characteristics = {
language: features.language,
framework: features.framework,
size: features.size,
complexity: features.complexity,
};
if (existing) {
existing.pattern.characteristics = characteristics;
existing.sampleSize += 1;
existing.confidence = Math.min(existing.sampleSize / 15, 1.0);
existing.lastUpdated = new Date().toISOString();
} else {
const pattern: LearningPattern = {
id: patternKey,
type: "project_similarity",
pattern: {
characteristics,
commonPatterns: [],
},
confidence: 0.1,
sampleSize: 1,
lastUpdated: new Date().toISOString(),
metadata: {
projectTypes: [features.language],
},
};
this.patterns.set(patternKey, pattern);
}
}
/**
* Update user behavior patterns from feedback
*/
private async updateUserBehaviorPattern(
features: ProjectFeatures,
feedback: Record<string, any>,
): Promise<void> {
const patternKey = this.generatePatternKey("user_behavior", features);
const existing = this.patterns.get(patternKey);
if (existing) {
existing.pattern.feedback = { ...existing.pattern.feedback, ...feedback };
existing.sampleSize += 1;
existing.confidence = Math.min(existing.sampleSize / 5, 1.0);
existing.lastUpdated = new Date().toISOString();
} else {
const pattern: LearningPattern = {
id: patternKey,
type: "user_behavior",
pattern: {
feedback,
preferences: {},
},
confidence: 0.2,
sampleSize: 1,
lastUpdated: new Date().toISOString(),
metadata: {},
};
this.patterns.set(patternKey, pattern);
}
}
/**
* Generate a consistent pattern key for grouping similar projects
*/
private generatePatternKey(type: string, features: ProjectFeatures): string {
const keyParts = [
type,
features.language,
features.framework || "none",
features.size,
features.complexity,
];
return keyParts.join("_").toLowerCase();
}
/**
* Get SSG preference pattern for similar projects
*/
private async getSSGPreferencePattern(
features: ProjectFeatures,
): Promise<LearningPattern | null> {
const patternKey = this.generatePatternKey("ssg_preference", features);
const pattern = this.patterns.get(patternKey);
if (pattern && pattern.sampleSize >= this.minSampleSize) {
return pattern;
}
// Try broader matching if exact match not found
const broaderKey = `ssg_preference_${features.language}_${features.size}`;
return this.patterns.get(broaderKey) || null;
}
/**
* Get deployment success pattern for risk assessment
*/
private async getDeploymentPattern(
features: ProjectFeatures,
): Promise<LearningPattern | null> {
const patternKey = this.generatePatternKey("deployment_success", features);
const pattern = this.patterns.get(patternKey);
if (pattern && pattern.sampleSize >= this.minSampleSize) {
return pattern;
}
return null;
}
/**
* Generate optimization insights based on learned patterns
*/
private async getOptimizationInsights(
features: ProjectFeatures,
): Promise<LearningInsight[]> {
const insights: LearningInsight[] = [];
// Check for common optimization opportunities
if (!features.hasTests) {
insights.push({
type: "optimization",
message: "Adding tests could improve deployment success rate by 25%",
confidence: 0.8,
actionable: true,
data: { optimization: "add_tests", impact: "deployment_success" },
});
}
if (!features.hasCI && features.size !== "small") {
insights.push({
type: "optimization",
message: "CI/CD setup recommended for projects of this size",
confidence: 0.7,
actionable: true,
data: { optimization: "add_ci", impact: "development_velocity" },
});
}
if (features.complexity === "complex" && !features.hasDocs) {
insights.push({
type: "optimization",
message:
"Complex projects benefit significantly from comprehensive documentation",
confidence: 0.9,
actionable: true,
data: { optimization: "improve_docs", impact: "maintainability" },
});
}
return insights;
}
/**
* Load patterns from persistent storage
*/
private async loadPatterns(): Promise<void> {
try {
const patternMemories =
await this.memoryManager.search("learning_pattern");
for (const memory of patternMemories) {
if (memory.data.pattern) {
this.patterns.set(memory.data.pattern.id, memory.data.pattern);
}
}
} catch (error) {
console.error("Failed to load learning patterns:", error);
}
}
/**
* Update patterns based on recent memory data
*/
private async updatePatterns(): Promise<void> {
// Analyze recent memories to update patterns
const recentMemories = await this.memoryManager.search("", {
sortBy: "timestamp",
});
const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // Last 7 days
for (const memory of recentMemories) {
if (new Date(memory.timestamp) > cutoffDate) {
// Infer outcome based on memory data
const outcome = this.inferOutcome(memory);
if (outcome) {
await this.learn(memory, outcome);
}
}
}
}
/**
* Infer outcome from memory entry data
*/
private inferOutcome(
memory: MemoryEntry,
): "success" | "failure" | "neutral" | null {
if (memory.type === "deployment") {
if (memory.data.status === "success") return "success";
if (memory.data.status === "failed") return "failure";
}
if (memory.type === "recommendation" && memory.data.feedback) {
if (memory.data.feedback.rating > 3) return "success";
if (memory.data.feedback.rating < 3) return "failure";
}
return "neutral";
}
/**
* Persist learned patterns to memory
*/
private async persistPatterns(): Promise<void> {
for (const [, pattern] of this.patterns) {
if (pattern.sampleSize >= this.minSampleSize) {
await this.memoryManager.remember(
"interaction",
{
pattern,
type: "learning_pattern",
},
{
tags: ["learning", "pattern", pattern.type],
},
);
}
}
}
/**
* Get all learned patterns
*/
async getPatterns(): Promise<LearningPattern[]> {
return Array.from(this.patterns.values());
}
/**
* Get learning statistics and insights
*/
async getStatistics(): Promise<{
totalPatterns: number;
patternsByType: Record<string, number>;
averageConfidence: number;
learningVelocity: number;
insights: string[];
}> {
const stats = {
totalPatterns: this.patterns.size,
patternsByType: {} as Record<string, number>,
averageConfidence: 0,
learningVelocity: 0,
insights: [] as string[],
};
let totalConfidence = 0;
for (const pattern of this.patterns.values()) {
stats.patternsByType[pattern.type] =
(stats.patternsByType[pattern.type] || 0) + 1;
totalConfidence += pattern.confidence;
}
stats.averageConfidence =
stats.totalPatterns > 0 ? totalConfidence / stats.totalPatterns : 0;
// Calculate learning velocity (patterns learned in last week)
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
stats.learningVelocity = Array.from(this.patterns.values()).filter(
(p) => new Date(p.lastUpdated) > weekAgo,
).length;
// Generate insights
if (stats.totalPatterns > 10) {
stats.insights.push(
`System has learned ${stats.totalPatterns} patterns with ${(
stats.averageConfidence * 100
).toFixed(0)}% average confidence`,
);
}
if (stats.learningVelocity > 0) {
stats.insights.push(
`Learning velocity: ${stats.learningVelocity} new patterns this week`,
);
}
const topPatternType = Object.entries(stats.patternsByType).sort(
([, a], [, b]) => b - a,
)[0];
if (topPatternType) {
stats.insights.push(
`Most common pattern type: ${topPatternType[0]} (${topPatternType[1]} patterns)`,
);
}
return stats;
}
/**
* Enable or disable learning
*/
setLearningEnabled(enabled: boolean): void {
this.learningEnabled = enabled;
}
/**
* Clear all learned patterns (useful for testing or reset)
*/
async clearPatterns(): Promise<void> {
this.patterns.clear();
}
}
export default IncrementalLearningSystem;
```
--------------------------------------------------------------------------------
/src/memory/integration.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Memory System Integration for DocuMCP
* Connects memory capabilities to MCP tools
*/
import { MemoryManager } from "./manager.js";
import { MemoryEntry } from "./storage.js";
let memoryManager: MemoryManager | null = null;
/**
* Initializes the DocuMCP memory system for persistent learning and context.
*
* Sets up the memory manager with optional custom storage directory, configures
* event listeners for debugging in development mode, and ensures the memory
* system is ready for storing and retrieving project analysis data, user
* preferences, and deployment patterns.
*
* @param storageDir - Optional custom directory path for memory storage (defaults to .documcp/memory)
*
* @returns Promise resolving to the initialized MemoryManager instance
*
* @throws {Error} When memory system initialization fails
* @throws {Error} When storage directory cannot be created or accessed
*
* @example
* ```typescript
* // Initialize with default storage
* const memory = await initializeMemory();
*
* // Initialize with custom storage directory
* const customMemory = await initializeMemory("/custom/memory/path");
* ```
*
* @since 1.0.0
* @version 1.2.0 - Added development mode event logging
*/
export async function initializeMemory(
storageDir?: string,
): Promise<MemoryManager> {
if (!memoryManager) {
memoryManager = new MemoryManager(storageDir);
await memoryManager.initialize();
// Set up event listeners (debug logging disabled in production)
if (process.env.NODE_ENV === "development") {
memoryManager.on("memory-created", (entry: MemoryEntry) => {
// eslint-disable-next-line no-console
console.log(`[Memory] Created: ${entry.id} (${entry.type})`);
});
memoryManager.on("memory-updated", (entry: MemoryEntry) => {
// eslint-disable-next-line no-console
console.log(`[Memory] Updated: ${entry.id}`);
});
memoryManager.on("memory-deleted", (id: string) => {
// eslint-disable-next-line no-console
console.log(`[Memory] Deleted: ${id}`);
});
}
}
return memoryManager;
}
/**
* Stores repository analysis data in the memory system for future reference.
*
* Persists comprehensive repository analysis results including structure, dependencies,
* documentation status, and recommendations. This data is used for learning patterns,
* improving future recommendations, and providing historical context for similar projects.
*
* @param projectPath - The file system path to the analyzed repository
* @param analysisData - The complete repository analysis results to store
*
* @returns Promise resolving to the unique memory entry ID
*
* @throws {Error} When memory system is not initialized
* @throws {Error} When analysis data cannot be stored
*
* @example
* ```typescript
* const analysisId = await rememberAnalysis("/path/to/project", {
* id: "analysis_123",
* structure: { totalFiles: 150, languages: { ".ts": 100 } },
* dependencies: { ecosystem: "javascript", packages: ["react"] },
* // ... other analysis data
* });
* ```
*
* @since 1.0.0
*/
export async function rememberAnalysis(
projectPath: string,
analysisData: any,
): Promise<string> {
const manager = await initializeMemory();
manager.setContext({
projectId: analysisData.projectId || projectPath,
repository: analysisData.repository?.url,
});
const entry = await manager.remember("analysis", analysisData, {
repository: analysisData.repository?.url,
tags: [
"analysis",
analysisData.language?.primary || "unknown",
analysisData.framework?.name || "none",
],
});
return entry.id;
}
/**
* Stores SSG recommendation data in the memory system for learning and pattern recognition.
*
* Persists recommendation results including the chosen SSG, confidence scores, reasoning,
* and alternatives. This data is used to improve future recommendations by learning from
* successful patterns and user choices.
*
* @param analysisId - The unique identifier of the associated repository analysis
* @param recommendation - The complete SSG recommendation results to store
*
* @returns Promise resolving to the unique memory entry ID
*
* @throws {Error} When memory system is not initialized
* @throws {Error} When recommendation data cannot be stored
* @throws {Error} When the associated analysis cannot be found
*
* @example
* ```typescript
* const recommendationId = await rememberRecommendation("analysis_123", {
* recommended: "docusaurus",
* confidence: 0.92,
* reasoning: ["React-based project", "Documentation focus"],
* alternatives: [
* { name: "hugo", score: 0.85, pros: ["Performance"], cons: ["Learning curve"] }
* ]
* });
* ```
*
* @since 1.0.0
*/
export async function rememberRecommendation(
analysisId: string,
recommendation: any,
): Promise<string> {
const manager = await initializeMemory();
const entry = await manager.remember("recommendation", recommendation, {
ssg: recommendation.recommended,
tags: ["recommendation", recommendation.recommended, "ssg"],
});
// Link to analysis
const analysis = await manager.recall(analysisId);
if (analysis) {
await manager.update(entry.id, {
metadata: {
...entry.metadata,
projectId: analysis.metadata.projectId,
},
});
}
return entry.id;
}
/**
* Stores deployment data in the memory system for success tracking and analytics.
*
* Persists deployment results including success status, timing, configuration used,
* and any issues encountered. This data enables deployment analytics, success rate
* tracking, and identification of deployment patterns for optimization.
*
* @param repository - The repository URL or identifier for the deployment
* @param deploymentData - The complete deployment results and metadata
*
* @returns Promise resolving to the unique memory entry ID
*
* @throws {Error} When memory system is not initialized
* @throws {Error} When deployment data cannot be stored
*
* @example
* ```typescript
* const deploymentId = await rememberDeployment("https://github.com/user/repo", {
* success: true,
* ssg: "docusaurus",
* deploymentTime: 180000,
* url: "https://user.github.io/repo",
* issues: [],
* configuration: { theme: "classic", plugins: ["search"] }
* });
* ```
*
* @since 1.0.0
*/
export async function rememberDeployment(
repository: string,
deploymentData: any,
): Promise<string> {
const manager = await initializeMemory();
manager.setContext({
projectId: repository,
repository,
});
const entry = await manager.remember("deployment", deploymentData, {
repository,
ssg: deploymentData.ssg,
tags: [
"deployment",
deploymentData.status || "unknown",
deploymentData.ssg,
],
});
return entry.id;
}
export async function rememberConfiguration(
projectName: string,
ssg: string,
configData: any,
): Promise<string> {
const manager = await initializeMemory();
manager.setContext({
projectId: projectName,
});
const entry = await manager.remember("configuration", configData, {
ssg,
tags: ["configuration", ssg, projectName],
});
return entry.id;
}
export async function recallProjectHistory(projectId: string): Promise<any> {
const manager = await initializeMemory();
const memories = await manager.search(
{ projectId },
{ sortBy: "timestamp", groupBy: "type" },
);
return {
projectId,
history: memories,
insights: await getProjectInsights(projectId),
};
}
/**
* Retrieves intelligent insights about a project based on historical data and patterns.
*
* Analyzes stored project data to provide actionable insights including technology
* trends, successful patterns, optimization opportunities, and recommendations for
* improvement. Uses machine learning and pattern recognition to generate contextual
* insights.
*
* @param projectId - The unique identifier of the project to analyze
*
* @returns Promise resolving to an array of insight strings
*
* @throws {Error} When memory system is not initialized
* @throws {Error} When project data cannot be retrieved
*
* @example
* ```typescript
* const insights = await getProjectInsights("project_abc123");
* console.log(insights);
* // Output: [
* // "Consider upgrading to Docusaurus v3 for better performance",
* // "Similar projects show 95% success rate with current configuration",
* // "Documentation could benefit from additional API examples"
* // ]
* ```
*
* @since 1.0.0
*/
export async function getProjectInsights(projectId: string): Promise<string[]> {
const manager = await initializeMemory();
const memories = await manager.search({ projectId });
const insights: string[] = [];
// Find patterns in SSG choices
const ssgMemories = memories.filter((m: any) => m.metadata.ssg);
if (ssgMemories.length > 0) {
const lastSSG = ssgMemories[ssgMemories.length - 1].metadata.ssg;
insights.push(`Previously used ${lastSSG} for this project`);
}
// Find deployment patterns
const deployments = memories.filter((m: any) => m.type === "deployment");
if (deployments.length > 0) {
const successful = deployments.filter(
(d: any) => d.data.status === "success",
).length;
const rate = ((successful / deployments.length) * 100).toFixed(0);
insights.push(`Deployment success rate: ${rate}%`);
}
// Find recent activity
const lastMemory = memories[memories.length - 1];
if (lastMemory) {
const daysAgo = Math.floor(
(Date.now() - new Date(lastMemory.timestamp).getTime()) /
(1000 * 60 * 60 * 24),
);
insights.push(`Last activity: ${daysAgo} days ago`);
}
return insights;
}
/**
* Finds similar projects based on analysis data and historical patterns.
*
* Uses similarity algorithms to identify projects with comparable characteristics
* including technology stack, project structure, documentation patterns, and
* deployment history. Useful for providing relevant examples and recommendations.
*
* @param analysisData - The analysis data to use for similarity comparison
* @param limit - Maximum number of similar projects to return (default: 5)
*
* @returns Promise resolving to an array of similar project data
*
* @throws {Error} When memory system is not initialized
* @throws {Error} When similarity analysis fails
*
* @example
* ```typescript
* const similar = await getSimilarProjects(analysisData, 3);
* console.log(similar.map(p => p.metadata.projectId));
* // Output: ["project_xyz", "project_abc", "project_def"]
* ```
*
* @since 1.0.0
*/
export async function getSimilarProjects(
analysisData: any,
limit: number = 5,
): Promise<any[]> {
const manager = await initializeMemory();
// Search for projects with similar characteristics
const similarProjects: any[] = [];
// Search by language
if (analysisData.language?.primary) {
const languageMatches = await manager.search(
{ tags: [analysisData.language.primary] },
{ sortBy: "timestamp" },
);
similarProjects.push(...languageMatches);
}
// Search by framework
if (analysisData.framework?.name) {
const frameworkMatches = await manager.search(
{ tags: [analysisData.framework.name] },
{ sortBy: "timestamp" },
);
similarProjects.push(...frameworkMatches);
}
// Deduplicate and return top matches
const unique = Array.from(
new Map(similarProjects.map((p) => [p.metadata.projectId, p])).values(),
);
return unique.slice(0, limit).map((project) => ({
projectId: project.metadata.projectId,
similarity: calculateSimilarity(analysisData, project.data),
recommendation: project.metadata.ssg,
timestamp: project.timestamp,
}));
}
function calculateSimilarity(data1: any, data2: any): number {
let score = 0;
// Language match
if (data1.language?.primary === data2.language?.primary) score += 0.3;
// Framework match
if (data1.framework?.name === data2.framework?.name) score += 0.3;
// Size similarity
if (Math.abs((data1.stats?.files || 0) - (data2.stats?.files || 0)) < 100)
score += 0.2;
// Documentation type match
if (data1.documentation?.type === data2.documentation?.type) score += 0.2;
return Math.min(score, 1.0);
}
export async function cleanupOldMemories(
daysToKeep: number = 30,
): Promise<number> {
const manager = await initializeMemory();
const cutoffDate = new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000);
return await manager.cleanup(cutoffDate);
}
export async function exportMemories(
format: "json" | "csv" = "json",
projectId?: string,
): Promise<string> {
const manager = await initializeMemory();
return await manager.export(format, projectId);
}
export async function importMemories(
data: string,
format: "json" | "csv" = "json",
): Promise<number> {
const manager = await initializeMemory();
return await manager.import(data, format);
}
export async function getMemoryStatistics(): Promise<any> {
const manager = await initializeMemory();
return await manager.analyze();
}
export function getMemoryManager(): MemoryManager | null {
return memoryManager;
}
export async function resetMemoryManager(storageDir?: string): Promise<void> {
if (memoryManager) {
await memoryManager.close();
}
memoryManager = null;
if (storageDir) {
await initializeMemory(storageDir);
}
}
// Memory handler functions for MCP tools
export async function handleMemoryRecall(args: {
query: string;
type?: string;
limit?: number;
}): Promise<any> {
const manager = await initializeMemory();
const searchOptions: any = {
sortBy: "timestamp",
limit: args.limit || 10,
};
if (args.type && args.type !== "all") {
searchOptions.type = args.type;
}
const memories = await manager.search({}, searchOptions);
return {
query: args.query,
type: args.type || "all",
count: memories.length,
memories: memories.map((m: any) => ({
id: m.id,
type: m.type,
timestamp: m.timestamp,
data: m.data,
metadata: m.metadata,
})),
};
}
export async function handleMemoryIntelligentAnalysis(args: {
projectPath: string;
baseAnalysis: any;
}): Promise<any> {
await initializeMemory();
// Get project history and similar projects for enhanced analysis
const projectId = args.baseAnalysis.projectId || args.projectPath;
const history = await recallProjectHistory(projectId);
const similarProjects = await getSimilarProjects(args.baseAnalysis);
// Enhance analysis with memory insights
const enhancedAnalysis = {
...args.baseAnalysis,
memoryInsights: {
projectHistory: history,
similarProjects,
patterns: await extractPatterns(args.baseAnalysis, history.history),
recommendations: await generateRecommendations(
args.baseAnalysis,
similarProjects,
),
},
};
// Remember this enhanced analysis
await rememberAnalysis(args.projectPath, enhancedAnalysis);
return enhancedAnalysis;
}
export async function handleMemoryEnhancedRecommendation(args: {
projectPath: string;
baseRecommendation: any;
projectFeatures: any;
}): Promise<any> {
await initializeMemory();
// Get similar projects with same features
const similarProjects = await getSimilarProjects(args.projectFeatures);
// Analyze success patterns
const successPatterns = await analyzeSuccessPatterns(similarProjects);
// Enhanced recommendation with memory insights
const enhancedRecommendation = {
...args.baseRecommendation,
memoryEnhanced: {
similarProjects,
successPatterns,
confidence: calculateConfidence(args.baseRecommendation, successPatterns),
alternativeOptions: await getAlternativeOptions(
args.baseRecommendation,
successPatterns,
),
},
};
return enhancedRecommendation;
}
// Helper functions for memory enhancement
async function extractPatterns(
analysis: any,
history: any[],
): Promise<string[]> {
const patterns: string[] = [];
// Analyze deployment patterns
const deployments = history.filter((h: any) => h.type === "deployment");
if (deployments.length > 0) {
const successfulDeployments = deployments.filter(
(d: any) => d.data.status === "success",
);
if (successfulDeployments.length > 0) {
patterns.push("Previous successful deployments found");
}
}
// Analyze SSG patterns
const recommendations = history.filter(
(h: any) => h.type === "recommendation",
);
if (recommendations.length > 0) {
const lastSSG =
recommendations[recommendations.length - 1].data.recommended;
patterns.push(`Previously recommended SSG: ${lastSSG}`);
}
return patterns;
}
async function generateRecommendations(
analysis: any,
similarProjects: any[],
): Promise<string[]> {
const recommendations: string[] = [];
if (similarProjects.length > 0) {
const popularSSG = findMostPopularSSG(similarProjects);
if (popularSSG) {
recommendations.push(
`Consider ${popularSSG} based on similar project success`,
);
}
}
return recommendations;
}
async function analyzeSuccessPatterns(similarProjects: any[]): Promise<any> {
const patterns = {
ssgSuccess: {} as Record<string, number>,
deploymentPatterns: [] as string[],
commonFeatures: [] as string[],
};
// Analyze SSG success rates
similarProjects.forEach((project: any) => {
const ssg = project.recommendation;
if (ssg) {
patterns.ssgSuccess[ssg] = (patterns.ssgSuccess[ssg] || 0) + 1;
}
});
return patterns;
}
function calculateConfidence(
baseRecommendation: any,
successPatterns: any,
): number {
const recommended = baseRecommendation.recommended;
const successCount = successPatterns.ssgSuccess[recommended] || 0;
const totalProjects = Object.values(
successPatterns.ssgSuccess as Record<string, number>,
).reduce((a: number, b: number) => a + b, 0);
if (totalProjects === 0) return 0.5; // Default confidence
return Math.min(successCount / totalProjects + 0.3, 1.0);
}
async function getAlternativeOptions(
baseRecommendation: any,
successPatterns: any,
): Promise<string[]> {
const sorted = Object.entries(successPatterns.ssgSuccess)
.sort(([, a]: [string, any], [, b]: [string, any]) => b - a)
.map(([ssg]) => ssg);
// Return top 2 alternatives different from the base recommendation
return sorted
.filter((ssg) => ssg !== baseRecommendation.recommended)
.slice(0, 2);
}
function findMostPopularSSG(projects: any[]): string | null {
const ssgCount: Record<string, number> = {};
projects.forEach((project) => {
const ssg = project.recommendation;
if (ssg) {
ssgCount[ssg] = (ssgCount[ssg] || 0) + 1;
}
});
const sorted = Object.entries(ssgCount).sort(([, a], [, b]) => b - a);
return sorted.length > 0 ? sorted[0][0] : null;
}
```
--------------------------------------------------------------------------------
/src/tools/check-documentation-links.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { readFile, readdir, stat } from "fs/promises";
import { join, extname, resolve, relative, dirname } from "path";
import { MCPToolResponse } from "../types/api.js";
// Input validation schema
const LinkCheckInputSchema = z.object({
documentation_path: z.string().default("./docs"),
check_external_links: z.boolean().default(true),
check_internal_links: z.boolean().default(true),
check_anchor_links: z.boolean().default(true),
timeout_ms: z.number().min(1000).max(30000).default(5000),
max_concurrent_checks: z.number().min(1).max(20).default(5),
allowed_domains: z.array(z.string()).default([]),
ignore_patterns: z.array(z.string()).default([]),
fail_on_broken_links: z.boolean().default(false),
output_format: z.enum(["summary", "detailed", "json"]).default("detailed"),
});
type LinkCheckInput = z.infer<typeof LinkCheckInputSchema>;
interface LinkCheckResult {
url: string;
status: "valid" | "broken" | "warning" | "skipped";
statusCode?: number;
error?: string;
responseTime?: number;
sourceFile: string;
lineNumber?: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}
interface LinkCheckReport {
summary: {
totalLinks: number;
validLinks: number;
brokenLinks: number;
warningLinks: number;
skippedLinks: number;
executionTime: number;
filesScanned: number;
};
results: LinkCheckResult[];
recommendations: string[];
configuration: {
checkExternalLinks: boolean;
checkInternalLinks: boolean;
checkAnchorLinks: boolean;
timeoutMs: number;
maxConcurrentChecks: number;
};
}
export async function checkDocumentationLinks(
input: Partial<LinkCheckInput>,
): Promise<MCPToolResponse<LinkCheckReport>> {
const startTime = Date.now();
try {
// Validate input with defaults
const validatedInput = LinkCheckInputSchema.parse(input);
const {
documentation_path,
check_external_links,
check_internal_links,
check_anchor_links,
timeout_ms,
max_concurrent_checks,
allowed_domains,
ignore_patterns,
fail_on_broken_links,
} = validatedInput;
// Scan documentation files
const documentationFiles = await scanDocumentationFiles(documentation_path);
if (documentationFiles.length === 0) {
return {
success: false,
error: {
code: "NO_DOCUMENTATION_FILES",
message: "No documentation files found in the specified path",
details: `Searched in: ${documentation_path}`,
resolution:
"Verify the documentation_path parameter points to a directory containing markdown files",
},
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
}
// Extract all links from documentation files
const allLinks = await extractLinksFromFiles(
documentationFiles,
documentation_path,
);
// Filter links based on configuration
const filteredLinks = filterLinks(allLinks, {
checkExternalLinks: check_external_links,
checkInternalLinks: check_internal_links,
checkAnchorLinks: check_anchor_links,
ignorePatterns: ignore_patterns,
});
// Check links with concurrency control
const linkResults = await checkLinksWithConcurrency(filteredLinks, {
timeoutMs: timeout_ms,
maxConcurrent: max_concurrent_checks,
allowedDomains: allowed_domains,
documentationPath: documentation_path,
});
// Generate report
const report = generateLinkCheckReport(linkResults, {
checkExternalLinks: check_external_links,
checkInternalLinks: check_internal_links,
checkAnchorLinks: check_anchor_links,
timeoutMs: timeout_ms,
maxConcurrentChecks: max_concurrent_checks,
filesScanned: documentationFiles.length,
executionTime: Date.now() - startTime,
});
// Check if we should fail on broken links
if (fail_on_broken_links && report.summary.brokenLinks > 0) {
return {
success: false,
error: {
code: "BROKEN_LINKS_FOUND",
message: `Found ${report.summary.brokenLinks} broken links`,
details: `${report.summary.brokenLinks} out of ${report.summary.totalLinks} links are broken`,
resolution:
"Fix the broken links or set fail_on_broken_links to false",
},
data: report,
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
}
return {
success: true,
data: report,
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
} catch (error) {
return {
success: false,
error: {
code: "LINK_CHECK_ERROR",
message: "Failed to check documentation links",
details:
error instanceof Error ? error.message : "Unknown error occurred",
resolution:
"Check the documentation path and ensure files are accessible",
},
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
}
}
async function scanDocumentationFiles(basePath: string): Promise<string[]> {
const files: string[] = [];
async function scanDirectory(dirPath: string): Promise<void> {
try {
const entries = await readdir(dirPath);
for (const entry of entries) {
const fullPath = join(dirPath, entry);
const stats = await stat(fullPath);
if (stats.isDirectory()) {
// Skip node_modules and hidden directories
if (!entry.startsWith(".") && entry !== "node_modules") {
await scanDirectory(fullPath);
}
} else if (stats.isFile()) {
const ext = extname(entry).toLowerCase();
if ([".md", ".mdx", ".markdown"].includes(ext)) {
files.push(fullPath);
}
}
}
} catch (error) {
// Skip directories we can't read
}
}
await scanDirectory(basePath);
return files;
}
async function extractLinksFromFiles(
files: string[],
basePath: string,
): Promise<
Array<{
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}>
> {
const allLinks: Array<{
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}> = [];
// Regex patterns for different link types
const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
const htmlLinkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
const refLinkRegex = /\[([^\]]+)\]:\s*(.+)/g;
for (const file of files) {
try {
const content = await readFile(file, "utf-8");
const lines = content.split("\n");
// Create proper relative file path
const absoluteBasePath = resolve(basePath);
const absoluteFilePath = resolve(file);
const relativeFile = relative(absoluteBasePath, absoluteFilePath).replace(
/\\/g,
"/",
);
// Extract markdown links
lines.forEach((line, index) => {
let match;
// Markdown links [text](url)
while ((match = markdownLinkRegex.exec(line)) !== null) {
const url = match[2].trim();
if (url && !url.startsWith("#")) {
// Skip empty and anchor-only links
allLinks.push({
url,
sourceFile: relativeFile,
lineNumber: index + 1,
linkType: determineLinkType(url),
});
}
}
// HTML links
while ((match = htmlLinkRegex.exec(line)) !== null) {
const url = match[1].trim();
if (url && !url.startsWith("#")) {
allLinks.push({
url,
sourceFile: relativeFile,
lineNumber: index + 1,
linkType: determineLinkType(url),
});
}
}
// Reference links
while ((match = refLinkRegex.exec(line)) !== null) {
const url = match[2].trim();
if (url && !url.startsWith("#")) {
allLinks.push({
url,
sourceFile: relativeFile,
lineNumber: index + 1,
linkType: determineLinkType(url),
});
}
}
});
} catch (error) {
// Skip files we can't read
}
}
return allLinks;
}
function determineLinkType(
url: string,
): "internal" | "external" | "anchor" | "mailto" | "tel" {
if (url.startsWith("mailto:")) return "mailto";
if (url.startsWith("tel:")) return "tel";
if (url.startsWith("#")) return "anchor";
if (url.startsWith("http://") || url.startsWith("https://"))
return "external";
return "internal";
}
function filterLinks(
links: Array<{
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}>,
options: {
checkExternalLinks: boolean;
checkInternalLinks: boolean;
checkAnchorLinks: boolean;
ignorePatterns: string[];
},
) {
return links.filter((link) => {
// Check if link should be ignored based on patterns
if (options.ignorePatterns.some((pattern) => link.url.includes(pattern))) {
return false;
}
// Filter by link type
switch (link.linkType) {
case "external":
return options.checkExternalLinks;
case "internal":
return options.checkInternalLinks;
case "anchor":
return options.checkAnchorLinks;
case "mailto":
case "tel":
return false; // Skip these for now
default:
return true;
}
});
}
async function checkLinksWithConcurrency(
links: Array<{
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}>,
options: {
timeoutMs: number;
maxConcurrent: number;
allowedDomains: string[];
documentationPath: string;
},
): Promise<LinkCheckResult[]> {
const results: LinkCheckResult[] = [];
async function checkSingleLink(link: {
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
}): Promise<LinkCheckResult> {
const startTime = Date.now();
try {
if (link.linkType === "internal") {
return await checkInternalLink(link, options.documentationPath);
} else if (link.linkType === "external") {
return await checkExternalLink(
link,
options.timeoutMs,
options.allowedDomains,
);
} else if (link.linkType === "anchor") {
return await checkAnchorLink(link, options.documentationPath);
}
return {
url: link.url,
status: "skipped",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
} catch (error) {
return {
url: link.url,
status: "broken",
error: error instanceof Error ? error.message : "Unknown error",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
}
// Process links with concurrency control
const chunks = [];
for (let i = 0; i < links.length; i += options.maxConcurrent) {
chunks.push(links.slice(i, i + options.maxConcurrent));
}
for (const chunk of chunks) {
const chunkResults = await Promise.all(chunk.map(checkSingleLink));
results.push(...chunkResults);
}
return results;
}
async function checkInternalLink(
link: {
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
},
documentationPath: string,
): Promise<LinkCheckResult> {
const startTime = Date.now();
try {
let targetPath = link.url;
// Remove anchor if present
const [filePath] = targetPath.split("#");
// Handle relative paths properly using Node.js path resolution
const absoluteDocPath = resolve(documentationPath);
const sourceFileAbsolutePath = resolve(absoluteDocPath, link.sourceFile);
const sourceDir = dirname(sourceFileAbsolutePath);
if (filePath.startsWith("./")) {
// Current directory reference - resolve relative to source file directory
targetPath = resolve(sourceDir, filePath.substring(2));
} else if (filePath.startsWith("../")) {
// Parent directory reference - resolve relative to source file directory
targetPath = resolve(sourceDir, filePath);
} else if (filePath.startsWith("/")) {
// Absolute path from documentation root
targetPath = resolve(absoluteDocPath, filePath.substring(1));
} else {
// Relative path - resolve relative to source file directory
targetPath = resolve(sourceDir, filePath);
}
try {
await stat(targetPath);
return {
url: link.url,
status: "valid",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
} catch {
return {
url: link.url,
status: "broken",
error: "File not found",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
} catch (error) {
return {
url: link.url,
status: "broken",
error: error instanceof Error ? error.message : "Unknown error",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
}
async function checkExternalLink(
link: {
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
},
timeoutMs: number,
allowedDomains: string[],
): Promise<LinkCheckResult> {
const startTime = Date.now();
try {
// Check if domain is in allowed list (if specified)
if (allowedDomains.length > 0) {
const url = new URL(link.url);
const isAllowed = allowedDomains.some(
(domain) =>
url.hostname === domain || url.hostname.endsWith("." + domain),
);
if (!isAllowed) {
return {
url: link.url,
status: "skipped",
error: "Domain not in allowed list",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
}
// Simple HEAD request to check if URL is accessible
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(link.url, {
method: "HEAD",
signal: controller.signal,
headers: {
"User-Agent": "DocuMCP Link Checker 1.0",
},
});
clearTimeout(timeoutId);
if (response.ok) {
return {
url: link.url,
status: "valid",
statusCode: response.status,
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
} else {
return {
url: link.url,
status: "broken",
statusCode: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
} catch (fetchError) {
clearTimeout(timeoutId);
if (fetchError instanceof Error && fetchError.name === "AbortError") {
return {
url: link.url,
status: "warning",
error: "Request timeout",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
throw fetchError;
}
} catch (error) {
return {
url: link.url,
status: "broken",
error: error instanceof Error ? error.message : "Unknown error",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
}
async function checkAnchorLink(
link: {
url: string;
sourceFile: string;
lineNumber: number;
linkType: "internal" | "external" | "anchor" | "mailto" | "tel";
},
_documentationPath: string,
): Promise<LinkCheckResult> {
const startTime = Date.now();
// For now, just mark anchor links as valid
// In a more sophisticated implementation, we would parse the target file
// and check if the anchor exists
return {
url: link.url,
status: "valid",
sourceFile: link.sourceFile,
lineNumber: link.lineNumber,
linkType: link.linkType,
responseTime: Date.now() - startTime,
};
}
function generateLinkCheckReport(
results: LinkCheckResult[],
config: {
checkExternalLinks: boolean;
checkInternalLinks: boolean;
checkAnchorLinks: boolean;
timeoutMs: number;
maxConcurrentChecks: number;
filesScanned: number;
executionTime: number;
},
): LinkCheckReport {
const summary = {
totalLinks: results.length,
validLinks: results.filter((r) => r.status === "valid").length,
brokenLinks: results.filter((r) => r.status === "broken").length,
warningLinks: results.filter((r) => r.status === "warning").length,
skippedLinks: results.filter((r) => r.status === "skipped").length,
executionTime: config.executionTime,
filesScanned: config.filesScanned,
};
const recommendations: string[] = [];
if (summary.brokenLinks > 0) {
recommendations.push(
`🔴 Fix ${summary.brokenLinks} broken links to improve documentation quality`,
);
}
if (summary.warningLinks > 0) {
recommendations.push(
`🟡 Review ${summary.warningLinks} warning links that may need attention`,
);
}
if (summary.validLinks === summary.totalLinks) {
recommendations.push(
"✅ All links are valid - excellent documentation quality!",
);
}
if (summary.totalLinks > 100) {
recommendations.push(
"📊 Consider implementing automated link checking in CI/CD pipeline",
);
}
return {
summary,
results,
recommendations,
configuration: {
checkExternalLinks: config.checkExternalLinks,
checkInternalLinks: config.checkInternalLinks,
checkAnchorLinks: config.checkAnchorLinks,
timeoutMs: config.timeoutMs,
maxConcurrentChecks: config.maxConcurrentChecks,
},
};
}
```
--------------------------------------------------------------------------------
/tests/tools/test-local-deployment.test.ts:
--------------------------------------------------------------------------------
```typescript
import { testLocalDeployment } from "../../src/tools/test-local-deployment.js";
import * as childProcess from "child_process";
import * as fs from "fs";
// Create simpler mocking approach
describe("testLocalDeployment", () => {
const testRepoPath = process.cwd();
afterEach(() => {
jest.restoreAllMocks();
});
describe("Input validation", () => {
it("should handle invalid SSG parameter", async () => {
await expect(
testLocalDeployment({
repositoryPath: "/test/path",
ssg: "invalid" as any,
}),
).rejects.toThrow();
});
it("should handle missing required parameters", async () => {
await expect(
testLocalDeployment({
ssg: "docusaurus",
} as any),
).rejects.toThrow();
});
it("should handle unsupported SSG gracefully", async () => {
// This should throw a ZodError due to input validation
await expect(
testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "gatsby" as any,
}),
).rejects.toThrow("Invalid enum value");
});
});
describe("Basic functionality", () => {
it("should return proper response structure", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
expect(result.content).toBeDefined();
expect(Array.isArray(result.content)).toBe(true);
expect(result.content.length).toBeGreaterThan(0);
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
});
it("should use default port when not specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(3000);
});
it("should use custom port when specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port: 4000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(4000);
});
it("should use custom timeout when specified", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
timeout: 120,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.buildSuccess).toBeDefined();
});
});
describe("SSG support", () => {
it("should handle all supported SSG types", async () => {
const ssgs = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"];
for (const ssg of ssgs) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe(ssg);
expect(parsedResult.buildSuccess).toBeDefined();
}
});
it("should generate test script for all SSG types", async () => {
const ssgs = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"];
for (const ssg of ssgs) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
port: 4000,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain(
`# Local Deployment Test Script for ${ssg}`,
);
expect(parsedResult.testScript).toContain("http://localhost:4000");
}
});
it("should include install commands for Node.js-based SSGs", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("npm install");
});
it("should not include install commands for non-Node.js SSGs", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).not.toContain("npm install");
});
});
describe("Configuration handling", () => {
it("should provide recommendations when configuration is missing", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll", // Jekyll config unlikely to exist in this repo
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.recommendations).toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
});
it("should provide next steps for missing configuration", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.nextSteps).toEqual(
expect.arrayContaining([expect.stringContaining("generate_config")]),
);
});
});
describe("Error handling", () => {
it("should handle general errors gracefully", async () => {
jest.spyOn(process, "chdir").mockImplementation(() => {
throw new Error("Permission denied");
});
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
});
// The tool returns an error response structure instead of throwing
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.success).toBe(false);
expect(parsedResult.error.code).toBe("LOCAL_TEST_FAILED");
expect(parsedResult.error.message).toContain("Permission denied");
});
it("should handle non-existent repository path", async () => {
const result = await testLocalDeployment({
repositoryPath: "/non/existent/path",
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should still work with skipBuild, but may have warnings
expect(parsedResult).toBeDefined();
expect(result.content).toBeDefined();
});
});
describe("Response structure validation", () => {
it("should include all required response fields", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult).toHaveProperty("buildSuccess");
expect(parsedResult).toHaveProperty("ssg");
expect(parsedResult).toHaveProperty("port");
expect(parsedResult).toHaveProperty("testScript");
expect(parsedResult).toHaveProperty("recommendations");
expect(parsedResult).toHaveProperty("nextSteps");
});
it("should include tool recommendations in next steps", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(Array.isArray(parsedResult.nextSteps)).toBe(true);
expect(parsedResult.nextSteps.length).toBeGreaterThan(0);
});
it("should validate test script content structure", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port: 8080,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
const testScript = parsedResult.testScript;
expect(testScript).toContain("# Local Deployment Test Script for hugo");
expect(testScript).toContain("http://localhost:8080");
expect(testScript).toContain("hugo server");
expect(testScript).toContain("--port 8080");
});
it("should handle different timeout values", async () => {
const timeouts = [30, 60, 120, 300];
for (const timeout of timeouts) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
timeout,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.buildSuccess).toBeDefined();
// Timeout is not directly returned in response, but test should pass
}
});
it("should provide appropriate recommendations for each SSG type", async () => {
const ssgConfigs = {
jekyll: "_config.yml",
hugo: "config.toml",
docusaurus: "docusaurus.config.js",
mkdocs: "mkdocs.yml",
eleventy: ".eleventy.js",
};
for (const [ssg, configFile] of Object.entries(ssgConfigs)) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: ssg as any,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.recommendations).toEqual(
expect.arrayContaining([expect.stringContaining(configFile)]),
);
}
});
it("should include comprehensive next steps", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll", // Missing config will trigger recommendations
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
const nextSteps = parsedResult.nextSteps;
expect(Array.isArray(nextSteps)).toBe(true);
expect(nextSteps.length).toBeGreaterThan(0);
// Should include generate_config step for missing config
expect(nextSteps).toEqual(
expect.arrayContaining([expect.stringContaining("generate_config")]),
);
});
it("should handle edge case with empty repository path", async () => {
const result = await testLocalDeployment({
repositoryPath: "",
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should handle gracefully and provide recommendations
expect(parsedResult).toBeDefined();
expect(result.content).toBeDefined();
});
it("should validate port range handling", async () => {
const ports = [1000, 3000, 8080, 9000, 65535];
for (const port of ports) {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
port,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.port).toBe(port);
expect(parsedResult.testScript).toContain(`http://localhost:${port}`);
}
});
});
describe("Advanced coverage scenarios", () => {
beforeEach(() => {
jest.spyOn(process, "chdir").mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("Configuration file scenarios", () => {
it("should detect existing configuration file for hugo", async () => {
// Mock fs.access to succeed for hugo config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for jekyll", async () => {
// Mock fs.access to succeed for jekyll config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for docusaurus", async () => {
// Mock fs.access to succeed for docusaurus config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for mkdocs", async () => {
// Mock fs.access to succeed for mkdocs config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
it("should detect existing configuration file for eleventy", async () => {
// Mock fs.access to succeed for eleventy config file
const mockFsAccess = jest
.spyOn(fs.promises, "access")
.mockResolvedValueOnce(undefined);
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
// Should not recommend missing config since file exists
expect(parsedResult.recommendations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("Missing configuration file"),
]),
);
mockFsAccess.mockRestore();
});
});
describe("Build scenarios with actual executions", () => {
it("should handle successful build for eleventy without skipBuild", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
skipBuild: false,
timeout: 10, // Short timeout to avoid long waits
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("eleventy");
expect(parsedResult.buildSuccess).toBeDefined();
expect(parsedResult.testScript).toContain("npx @11ty/eleventy");
});
it("should handle successful build for mkdocs without skipBuild", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
skipBuild: false,
timeout: 10, // Short timeout to avoid long waits
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("mkdocs");
expect(parsedResult.buildSuccess).toBeDefined();
expect(parsedResult.testScript).toContain("mkdocs build");
});
it("should exercise server start paths with short timeout", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "hugo",
skipBuild: true,
timeout: 5, // Very short timeout to trigger timeout path
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.ssg).toBe("hugo");
expect(parsedResult.serverStarted).toBeDefined();
// localUrl may be undefined if server doesn't start quickly enough
expect(
typeof parsedResult.localUrl === "string" ||
parsedResult.localUrl === undefined,
).toBe(true);
});
it("should test port customization in serve commands", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "jekyll",
port: 4000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--port 4000");
expect(parsedResult.testScript).toContain("http://localhost:4000");
});
it("should test mkdocs serve command with custom port", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "mkdocs",
port: 8000,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--dev-addr localhost:8000");
expect(parsedResult.testScript).toContain("http://localhost:8000");
});
it("should test eleventy serve command with custom port", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "eleventy",
port: 3001,
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.testScript).toContain("--port 3001");
expect(parsedResult.testScript).toContain("http://localhost:3001");
});
it("should provide correct next steps recommendations", async () => {
const result = await testLocalDeployment({
repositoryPath: testRepoPath,
ssg: "docusaurus",
skipBuild: true,
});
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.nextSteps).toBeDefined();
expect(Array.isArray(parsedResult.nextSteps)).toBe(true);
expect(parsedResult.nextSteps.length).toBeGreaterThan(0);
});
});
});
});
```
--------------------------------------------------------------------------------
/tests/tools/recommend-ssg.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Test suite for SSG Recommendation Tool
*/
import { jest } from "@jest/globals";
import { recommendSSG } from "../../src/tools/recommend-ssg.js";
// Mock the memory and KG integration
jest.mock("../../src/memory/kg-integration.js", () => ({
getMemoryManager: jest.fn(),
getKnowledgeGraph: jest.fn(),
getUserPreferenceManager: jest.fn(),
getProjectContext: jest.fn(),
saveKnowledgeGraph: (jest.fn() as any).mockResolvedValue(undefined),
}));
describe("recommendSSG", () => {
let mockManager: any;
let mockKG: any;
let mockPreferenceManager: any;
beforeEach(() => {
mockManager = {
recall: jest.fn() as any,
} as any;
mockKG = {
findNode: (jest.fn() as any).mockResolvedValue(null),
findNodes: (jest.fn() as any).mockResolvedValue([]),
findEdges: (jest.fn() as any).mockResolvedValue([]),
getAllNodes: (jest.fn() as any).mockResolvedValue([]),
addNode: (jest.fn() as any).mockImplementation((node: any) => node),
addEdge: (jest.fn() as any).mockReturnValue(undefined),
} as any;
mockPreferenceManager = {
getPreference: (jest.fn() as any).mockResolvedValue(null),
} as any;
const {
getMemoryManager,
getKnowledgeGraph,
getUserPreferenceManager,
getProjectContext,
} = require("../../src/memory/kg-integration.js");
getMemoryManager.mockResolvedValue(mockManager);
getKnowledgeGraph.mockResolvedValue(mockKG);
getUserPreferenceManager.mockResolvedValue(mockPreferenceManager);
getProjectContext.mockResolvedValue({
previousAnalyses: 0,
lastAnalyzed: null,
knownTechnologies: [],
similarProjects: [],
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe("Input Validation", () => {
it("should validate required analysisId parameter", async () => {
await expect(recommendSSG({})).rejects.toThrow();
});
it("should validate analysisId as string", async () => {
await expect(recommendSSG({ analysisId: 123 })).rejects.toThrow();
});
it("should accept valid preferences", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "test-id",
preferences: {
priority: "simplicity",
ecosystem: "javascript",
},
});
expect(result.content).toBeDefined();
});
it("should reject invalid priority preference", async () => {
await expect(
recommendSSG({
analysisId: "test-id",
preferences: { priority: "invalid" },
}),
).rejects.toThrow();
});
it("should reject invalid ecosystem preference", async () => {
await expect(
recommendSSG({
analysisId: "test-id",
preferences: { ecosystem: "invalid" },
}),
).rejects.toThrow();
});
});
describe("Memory Integration", () => {
it("should retrieve analysis from memory when available", async () => {
const mockAnalysis = {
data: {
content: [
{
type: "text",
text: JSON.stringify({
repository: { language: "JavaScript" },
complexity: "low",
size: "small",
}),
},
],
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
expect(mockManager.recall).toHaveBeenCalledWith("test-id");
expect(result.content).toBeDefined();
});
it("should handle missing analysis gracefully", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "non-existent-id",
});
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe("text");
});
it("should handle analysis with direct data structure", async () => {
const mockAnalysis = {
data: {
repository: { language: "Python" },
complexity: "medium",
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
it("should handle corrupted analysis data", async () => {
const mockAnalysis = {
data: {
content: [
{
type: "text",
text: "invalid json",
},
],
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
});
describe("SSG Recommendations", () => {
it("should recommend Jekyll for Ruby projects", async () => {
const mockAnalysis = {
data: {
dependencies: {
ecosystem: "ruby",
},
complexity: "low",
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("jekyll");
});
it("should recommend Hugo for Go projects", async () => {
const mockAnalysis = {
data: {
dependencies: {
ecosystem: "go",
},
complexity: "medium",
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("hugo");
});
it("should recommend Docusaurus for JavaScript projects", async () => {
const mockAnalysis = {
data: {
dependencies: {
ecosystem: "javascript",
},
documentation: {
estimatedComplexity: "complex",
},
recommendations: {
teamSize: "large",
},
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("docusaurus");
});
it("should recommend MkDocs for Python projects", async () => {
const mockAnalysis = {
data: {
dependencies: {
ecosystem: "python",
},
complexity: "medium",
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("mkdocs");
});
it("should recommend Eleventy for simple JavaScript projects with simplicity priority", async () => {
const mockAnalysis = {
data: {
dependencies: {
ecosystem: "javascript",
},
documentation: {
estimatedComplexity: "simple",
},
recommendations: {
teamSize: "small",
},
},
};
mockManager.recall.mockResolvedValue(mockAnalysis);
const result = await recommendSSG({
analysisId: "test-id",
preferences: { priority: "simplicity" },
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("eleventy");
});
});
describe("Preference-based Recommendations", () => {
it("should prioritize simplicity when requested", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "test-id",
preferences: { priority: "simplicity" },
});
const recommendation = JSON.parse(result.content[0].text);
expect(["jekyll", "eleventy"]).toContain(recommendation.recommended);
});
it("should consider ecosystem preferences", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "test-id",
preferences: { ecosystem: "javascript" },
});
const recommendation = JSON.parse(result.content[0].text);
expect(["docusaurus", "eleventy"]).toContain(recommendation.recommended);
});
it("should handle performance preference with fallback", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "test-id",
preferences: { priority: "performance" },
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("docusaurus");
});
it("should handle features preference with fallback", async () => {
mockManager.recall.mockResolvedValue(null);
const result = await recommendSSG({
analysisId: "test-id",
preferences: { priority: "features" },
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("docusaurus");
});
});
describe("Scoring and Alternatives", () => {
it("should provide confidence scores", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
},
complexity: "medium",
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.confidence).toBeGreaterThan(0);
expect(recommendation.confidence).toBeLessThanOrEqual(1);
});
it("should provide alternative recommendations", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
},
complexity: "medium",
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.alternatives).toBeDefined();
expect(Array.isArray(recommendation.alternatives)).toBe(true);
expect(recommendation.alternatives.length).toBeGreaterThan(0);
});
it("should include pros and cons for alternatives", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "python",
},
complexity: "medium",
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
const alternative = recommendation.alternatives[0];
expect(alternative.name).toBeDefined();
expect(alternative.score).toBeDefined();
expect(Array.isArray(alternative.pros)).toBe(true);
expect(Array.isArray(alternative.cons)).toBe(true);
});
it("should sort alternatives by score", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
},
documentation: {
estimatedComplexity: "complex",
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
const scores = recommendation.alternatives.map((alt: any) => alt.score);
for (let i = 1; i < scores.length; i++) {
expect(scores[i]).toBeLessThanOrEqual(scores[i - 1]);
}
});
});
describe("Complex Project Analysis", () => {
it("should handle projects with React packages", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
packages: ["react", "next"],
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBe("docusaurus");
expect(recommendation.reasoning.length).toBeGreaterThan(0);
});
it("should consider project size in recommendations", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
},
structure: {
totalFiles: 150, // Large project
},
documentation: {
estimatedComplexity: "complex",
hasReadme: true,
hasDocs: true,
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.confidence).toBeGreaterThan(0.85); // Should have higher confidence with more data
});
it("should handle missing ecosystem information", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "unknown",
},
documentation: {
estimatedComplexity: "moderate",
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBeDefined();
});
it("should consider existing documentation structure", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "javascript",
},
documentation: {
hasReadme: true,
hasDocs: true,
estimatedComplexity: "moderate",
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.confidence).toBeGreaterThan(0.85); // Higher confidence with documentation
});
});
describe("Memory Error Handling", () => {
it("should handle memory initialization failure", async () => {
const {
getMemoryManager,
} = require("../../src/memory/kg-integration.js");
getMemoryManager.mockRejectedValue(new Error("Memory failed"));
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe("text");
// Reset the mock
getMemoryManager.mockResolvedValue(mockManager);
});
it("should handle memory recall failure", async () => {
mockManager.recall.mockRejectedValue(new Error("Recall failed"));
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
it("should handle corrupted memory data", async () => {
mockManager.recall.mockResolvedValue({
data: {
content: [
{
type: "text",
text: '{"invalid": json}',
},
],
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
});
describe("Performance and Timing", () => {
it("should complete recommendation in reasonable time", async () => {
mockManager.recall.mockResolvedValue({
data: {
repository: { language: "JavaScript" },
complexity: "medium",
},
});
const start = Date.now();
await recommendSSG({
analysisId: "test-id",
});
const duration = Date.now() - start;
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
});
it("should include execution time in response", async () => {
mockManager.recall.mockResolvedValue({
data: {
repository: { language: "JavaScript" },
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content[1].text).toContain("Execution completed in");
});
});
describe("Edge Cases", () => {
it("should handle null analysis data", async () => {
mockManager.recall.mockResolvedValue({
data: null,
});
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
it("should handle empty analysis data", async () => {
mockManager.recall.mockResolvedValue({
data: {},
});
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result.content).toBeDefined();
});
it("should handle analysis without dependency data", async () => {
mockManager.recall.mockResolvedValue({
data: {
documentation: {
estimatedComplexity: "moderate",
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBeDefined();
});
it("should handle unknown programming languages", async () => {
mockManager.recall.mockResolvedValue({
data: {
dependencies: {
ecosystem: "unknown",
},
documentation: {
estimatedComplexity: "moderate",
},
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation.recommended).toBeDefined();
});
});
describe("Response Format", () => {
it("should return properly formatted MCP response", async () => {
mockManager.recall.mockResolvedValue({
data: {
repository: { language: "JavaScript" },
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
expect(result).toHaveProperty("content");
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty("type");
expect(result.content[0]).toHaveProperty("text");
});
it("should include all required recommendation fields", async () => {
mockManager.recall.mockResolvedValue({
data: {
repository: { language: "Python" },
},
});
const result = await recommendSSG({
analysisId: "test-id",
});
const recommendation = JSON.parse(result.content[0].text);
expect(recommendation).toHaveProperty("recommended");
expect(recommendation).toHaveProperty("confidence");
expect(recommendation).toHaveProperty("reasoning");
expect(recommendation).toHaveProperty("alternatives");
expect(result.content[1].text).toContain("Execution completed in");
});
});
});
```
--------------------------------------------------------------------------------
/tests/tools/track-documentation-freshness.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Integration Tests for track_documentation_freshness Tool
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import fs from "fs/promises";
import path from "path";
import os from "os";
import {
trackDocumentationFreshness,
type TrackDocumentationFreshnessInput,
} from "../../src/tools/track-documentation-freshness.js";
// Example git SHA for testing
const SHA_EXAMPLE = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0";
describe("track_documentation_freshness Tool", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "track-freshness-test-"));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
describe("Basic Functionality", () => {
it("should track freshness with preset thresholds", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
// Create test files
const now = Date.now();
await fs.writeFile(
path.join(docsPath, "fresh.md"),
`---
documcp:
last_updated: "${new Date(now - 1 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Fresh Doc`,
);
await fs.writeFile(
path.join(docsPath, "old.md"),
`---
documcp:
last_updated: "${new Date(now - 60 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Old Doc`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.report.totalFiles).toBe(2);
expect(result.data.report.freshFiles).toBeGreaterThan(0);
expect(result.metadata.executionTime).toBeGreaterThan(0);
});
it("should track freshness with custom thresholds", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(
path.join(docsPath, "test.md"),
`---
documcp:
last_updated: "${new Date(Date.now() - 45 * 60 * 1000).toISOString()}"
---
# Test`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
warningThreshold: { value: 30, unit: "minutes" },
staleThreshold: { value: 1, unit: "hours" },
criticalThreshold: { value: 2, unit: "hours" },
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report).toBeDefined();
expect(result.data.thresholds.warning).toEqual({
value: 30,
unit: "minutes",
});
});
it("should identify files without metadata", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(
path.join(docsPath, "no-metadata.md"),
"# No Metadata",
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report.filesWithoutMetadata).toBe(1);
expect(result.data.report.totalFiles).toBe(1);
});
});
describe("Staleness Levels", () => {
it("should correctly categorize fresh files", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(
path.join(docsPath, "fresh.md"),
`---
documcp:
last_updated: "${new Date(
Date.now() - 2 * 24 * 60 * 60 * 1000,
).toISOString()}"
---
# Fresh`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.freshFiles).toBe(1);
expect(result.data.report.staleFiles).toBe(0);
expect(result.data.report.criticalFiles).toBe(0);
});
it("should correctly categorize stale files", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(
path.join(docsPath, "stale.md"),
`---
documcp:
last_updated: "${new Date(
Date.now() - 70 * 24 * 60 * 60 * 1000,
).toISOString()}"
---
# Stale`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.staleFiles).toBeGreaterThan(0);
});
it("should correctly categorize critical files", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(
path.join(docsPath, "critical.md"),
`---
documcp:
last_updated: "${new Date(
Date.now() - 100 * 24 * 60 * 60 * 1000,
).toISOString()}"
---
# Critical`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.criticalFiles).toBe(1);
});
});
describe("File Listing Options", () => {
it("should include file list when requested", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
includeFileList: true,
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.files).toBeDefined();
expect(result.data.report.files.length).toBe(1);
expect(result.data.formattedReport).toContain("File Details");
});
it("should exclude file list when not requested", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
includeFileList: false,
};
const result = await trackDocumentationFreshness(input);
expect(result.data.formattedReport).not.toContain("File Details");
});
});
describe("Sorting Options", () => {
it("should sort files by staleness", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
const now = Date.now();
await fs.writeFile(
path.join(docsPath, "fresh.md"),
`---
documcp:
last_updated: "${new Date(now - 1 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Fresh`,
);
await fs.writeFile(
path.join(docsPath, "stale.md"),
`---
documcp:
last_updated: "${new Date(now - 60 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Stale`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
sortBy: "staleness",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
// Stale files should appear first when sorted by staleness
const formattedReport = result.data.formattedReport;
const staleIndex = formattedReport.indexOf("stale.md");
const freshIndex = formattedReport.indexOf("fresh.md");
if (staleIndex !== -1 && freshIndex !== -1) {
expect(staleIndex).toBeLessThan(freshIndex);
}
});
it("should sort files by age", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
const now = Date.now();
await fs.writeFile(
path.join(docsPath, "newer.md"),
`---
documcp:
last_updated: "${new Date(now - 10 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Newer`,
);
await fs.writeFile(
path.join(docsPath, "older.md"),
`---
documcp:
last_updated: "${new Date(now - 50 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Older`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
sortBy: "age",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
});
});
describe("Nested Directories", () => {
it("should scan nested directories recursively", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.mkdir(path.join(docsPath, "api"));
await fs.mkdir(path.join(docsPath, "guides"));
await fs.writeFile(path.join(docsPath, "index.md"), "# Index");
await fs.writeFile(path.join(docsPath, "api", "endpoints.md"), "# API");
await fs.writeFile(
path.join(docsPath, "guides", "tutorial.md"),
"# Guide",
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.totalFiles).toBe(3);
});
it("should skip common ignored directories", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.mkdir(path.join(docsPath, "node_modules"));
await fs.mkdir(path.join(docsPath, ".git"));
await fs.writeFile(path.join(docsPath, "index.md"), "# Index");
await fs.writeFile(
path.join(docsPath, "node_modules", "skip.md"),
"# Skip",
);
await fs.writeFile(path.join(docsPath, ".git", "skip.md"), "# Skip");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.report.totalFiles).toBe(1);
});
});
describe("Error Handling", () => {
it("should handle non-existent directory", async () => {
const input: TrackDocumentationFreshnessInput = {
docsPath: "/nonexistent/path",
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.error?.code).toBe("FRESHNESS_TRACKING_FAILED");
});
it("should handle empty directory", async () => {
const docsPath = path.join(tempDir, "empty-docs");
await fs.mkdir(docsPath);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report.totalFiles).toBe(0);
});
});
describe("Preset Thresholds", () => {
const presets: Array<
keyof typeof import("../../src/utils/freshness-tracker.js").STALENESS_PRESETS
> = ["realtime", "active", "recent", "weekly", "monthly", "quarterly"];
presets.forEach((preset) => {
it(`should work with ${preset} preset`, async () => {
const docsPath = path.join(tempDir, `docs-${preset}`);
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset,
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.thresholds).toBeDefined();
});
});
});
describe("Output Format", () => {
it("should include formatted report in response", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.formattedReport).toBeDefined();
expect(result.data.formattedReport).toContain(
"Documentation Freshness Report",
);
expect(result.data.formattedReport).toContain("Summary Statistics");
expect(result.data.formattedReport).toContain("Freshness Breakdown");
});
it("should include summary in response", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.data.summary).toBeDefined();
expect(result.data.summary).toContain("Scanned");
expect(result.data.summary).toContain("files");
});
it("should include metadata in response", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.metadata).toBeDefined();
expect(result.metadata.toolVersion).toBe("1.0.0");
expect(result.metadata.timestamp).toBeDefined();
expect(result.metadata.executionTime).toBeGreaterThanOrEqual(0);
});
it("should handle KG storage disabled", async () => {
const docsPath = path.join(tempDir, "docs");
const projectPath = tempDir;
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
projectPath,
preset: "monthly",
storeInKG: false,
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.kgInsights).toBeUndefined();
});
it("should handle projectPath without KG storage", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "test.md"), "# Test");
const input: TrackDocumentationFreshnessInput = {
docsPath,
// No projectPath provided
preset: "monthly",
storeInKG: true, // Won't store because projectPath is missing
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
});
it("should handle error gracefully", async () => {
const input: TrackDocumentationFreshnessInput = {
docsPath: "/nonexistent/path/that/does/not/exist",
preset: "monthly",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.error?.code).toBe("FRESHNESS_TRACKING_FAILED");
expect(result.metadata).toBeDefined();
});
it("should sort files by age", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
const now = Date.now();
await fs.writeFile(
path.join(docsPath, "newer.md"),
`---
documcp:
last_updated: "${new Date(now - 1 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Newer`,
);
await fs.writeFile(
path.join(docsPath, "older.md"),
`---
documcp:
last_updated: "${new Date(now - 10 * 24 * 60 * 60 * 1000).toISOString()}"
---
# Older`,
);
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
sortBy: "age",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report.files.length).toBe(2);
});
it("should sort files by path", async () => {
const docsPath = path.join(tempDir, "docs");
await fs.mkdir(docsPath);
await fs.writeFile(path.join(docsPath, "z.md"), "# Z");
await fs.writeFile(path.join(docsPath, "a.md"), "# A");
const input: TrackDocumentationFreshnessInput = {
docsPath,
preset: "monthly",
sortBy: "path",
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
});
it("should display commit hash for files validated against commits", async () => {
const docsPath = path.join(tempDir, "docs");
const projectPath = tempDir;
await fs.mkdir(docsPath);
// Create file with validated_against_commit metadata
const fileContent = `---
documcp:
last_updated: ${new Date().toISOString()}
last_validated: ${new Date().toISOString()}
validated_against_commit: ${SHA_EXAMPLE}
---
# Test Document
Content`;
await fs.writeFile(path.join(docsPath, "test.md"), fileContent);
const input: TrackDocumentationFreshnessInput = {
docsPath,
projectPath,
preset: "monthly",
includeFileList: true,
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.formattedReport).toContain(
SHA_EXAMPLE.substring(0, 7),
);
});
it("should format warning recommendations correctly", async () => {
const docsPath = path.join(tempDir, "docs");
const projectPath = tempDir;
await fs.mkdir(docsPath);
// Create a file with warning-level staleness
const warnDate = new Date();
warnDate.setDate(warnDate.getDate() - 45); // 45 days ago (monthly preset: warning=30d, stale=60d, critical=90d)
const fileContent = `---
documcp:
last_updated: ${warnDate.toISOString()}
last_validated: ${warnDate.toISOString()}
---
# Test Document`;
await fs.writeFile(path.join(docsPath, "warn.md"), fileContent);
const input: TrackDocumentationFreshnessInput = {
docsPath,
projectPath,
preset: "monthly",
storeInKG: true,
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report.warningFiles).toBeGreaterThan(0);
});
it("should format critical recommendations correctly", async () => {
const docsPath = path.join(tempDir, "docs");
const projectPath = tempDir;
await fs.mkdir(docsPath);
// Create a file with critical-level staleness
const criticalDate = new Date();
criticalDate.setDate(criticalDate.getDate() - 100); // 100 days ago (critical for monthly preset)
const fileContent = `---
documcp:
last_updated: ${criticalDate.toISOString()}
last_validated: ${criticalDate.toISOString()}
---
# Old Document`;
await fs.writeFile(path.join(docsPath, "critical.md"), fileContent);
const input: TrackDocumentationFreshnessInput = {
docsPath,
projectPath,
preset: "monthly",
storeInKG: true,
};
const result = await trackDocumentationFreshness(input);
expect(result.success).toBe(true);
expect(result.data.report.criticalFiles).toBeGreaterThan(0);
});
});
});
```
--------------------------------------------------------------------------------
/tests/utils/ast-analyzer.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* AST Analyzer Tests (Phase 3)
*/
import {
ASTAnalyzer,
FunctionSignature,
ClassInfo,
} from "../../src/utils/ast-analyzer.js";
import { promises as fs } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { mkdtemp, rm } from "fs/promises";
describe("ASTAnalyzer", () => {
let analyzer: ASTAnalyzer;
let tempDir: string;
beforeAll(async () => {
analyzer = new ASTAnalyzer();
await analyzer.initialize();
tempDir = await mkdtemp(join(tmpdir(), "ast-test-"));
});
afterAll(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe("TypeScript/JavaScript Analysis", () => {
test("should extract function declarations", async () => {
const code = `
export async function testFunction(param1: string, param2: number): Promise<void> {
console.log(param1, param2);
}
export function syncFunction(name: string): string {
return name.toUpperCase();
}
`.trim();
const filePath = join(tempDir, "test-functions.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.functions).toHaveLength(2);
const asyncFunc = result?.functions.find(
(f) => f.name === "testFunction",
);
expect(asyncFunc).toBeDefined();
expect(asyncFunc?.isAsync).toBe(true);
expect(asyncFunc?.isExported).toBe(true);
expect(asyncFunc?.parameters).toHaveLength(2);
expect(asyncFunc?.returnType).toBe("Promise");
const syncFunc = result?.functions.find((f) => f.name === "syncFunction");
expect(syncFunc).toBeDefined();
expect(syncFunc?.isAsync).toBe(false);
expect(syncFunc?.returnType).toBe("string");
});
test("should extract arrow function declarations", async () => {
const code = `
export const arrowFunc = async (x: number, y: number): Promise<number> => {
return x + y;
};
const privateFunc = (name: string) => {
return name.toLowerCase();
};
`.trim();
const filePath = join(tempDir, "test-arrow.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.functions).toHaveLength(2);
const exportedArrow = result?.functions.find(
(f) => f.name === "arrowFunc",
);
expect(exportedArrow).toBeDefined();
expect(exportedArrow?.isAsync).toBe(true);
expect(exportedArrow?.parameters).toHaveLength(2);
});
test("should extract class information", async () => {
const code = `
/**
* Test class documentation
*/
export class TestClass extends BaseClass {
private value: number;
public readonly name: string;
constructor(name: string) {
super();
this.name = name;
this.value = 0;
}
/**
* Public method
*/
public async getValue(): Promise<number> {
return this.value;
}
private setValue(val: number): void {
this.value = val;
}
}
`.trim();
const filePath = join(tempDir, "test-class.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.classes).toHaveLength(1);
const testClass = result?.classes[0];
expect(testClass?.name).toBe("TestClass");
expect(testClass?.isExported).toBe(true);
expect(testClass?.extends).toBe("BaseClass");
expect(testClass?.properties).toHaveLength(2);
expect(testClass?.methods.length).toBeGreaterThan(0);
const publicMethod = testClass?.methods.find(
(m) => m.name === "getValue",
);
expect(publicMethod).toBeDefined();
expect(publicMethod?.isAsync).toBe(true);
expect(publicMethod?.isPublic).toBe(true);
});
test("should extract interface information", async () => {
const code = `
/**
* User interface
*/
export interface User {
id: string;
name: string;
age: number;
readonly email: string;
getProfile(): Promise<Profile>;
}
interface Profile {
bio: string;
}
`.trim();
const filePath = join(tempDir, "test-interface.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.interfaces).toHaveLength(2);
const userInterface = result?.interfaces.find((i) => i.name === "User");
expect(userInterface).toBeDefined();
expect(userInterface?.isExported).toBe(true);
expect(userInterface?.properties).toHaveLength(4);
expect(userInterface?.methods).toHaveLength(1);
const emailProp = userInterface?.properties.find(
(p) => p.name === "email",
);
expect(emailProp?.isReadonly).toBe(true);
});
test("should extract type aliases", async () => {
const code = `
export type ID = string | number;
export type Status = "pending" | "active" | "inactive";
type PrivateType = { x: number; y: number };
`.trim();
const filePath = join(tempDir, "test-types.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.types).toHaveLength(3);
const idType = result?.types.find((t) => t.name === "ID");
expect(idType?.isExported).toBe(true);
});
test("should extract imports and exports", async () => {
const code = `
import { func1, func2 } from "./module1";
import type { Type1 } from "./types";
import defaultExport from "./default";
export { func1, func2 };
export default class MyClass {}
`.trim();
const filePath = join(tempDir, "test-imports.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.imports.length).toBeGreaterThan(0);
expect(result?.exports).toContain("func1");
expect(result?.exports).toContain("func2");
});
test("should calculate complexity metrics", async () => {
const code = `
export function complexFunction(x: number): number {
if (x > 10) {
for (let i = 0; i < x; i++) {
if (i % 2 === 0) {
try {
return i;
} catch (error) {
continue;
}
}
}
} else {
return 0;
}
return -1;
}
`.trim();
const filePath = join(tempDir, "test-complexity.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const func = result?.functions[0];
expect(func?.complexity).toBeGreaterThan(1);
});
test("should extract JSDoc comments", async () => {
const code = `
/**
* This function adds two numbers
* @param a First number
* @param b Second number
* @returns The sum
*/
export function add(a: number, b: number): number {
return a + b;
}
`.trim();
const filePath = join(tempDir, "test-jsdoc.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const func = result?.functions[0];
expect(func?.docComment).toBeTruthy();
expect(func?.docComment).toContain("adds two numbers");
});
});
describe("Drift Detection", () => {
test("should detect function signature changes", async () => {
const oldCode = `
export function processData(data: string): void {
console.log(data);
}
`.trim();
const newCode = `
export function processData(data: string, options: object): Promise<string> {
console.log(data, options);
return Promise.resolve("done");
}
`.trim();
const oldFile = join(tempDir, "old-file.ts");
const newFile = join(tempDir, "new-file.ts");
await fs.writeFile(oldFile, oldCode);
await fs.writeFile(newFile, newCode);
const oldAnalysis = await analyzer.analyzeFile(oldFile);
const newAnalysis = await analyzer.analyzeFile(newFile);
expect(oldAnalysis).not.toBeNull();
expect(newAnalysis).not.toBeNull();
const diffs = await analyzer.detectDrift(oldAnalysis!, newAnalysis!);
expect(diffs.length).toBeGreaterThan(0);
const funcDiff = diffs.find(
(d) => d.category === "function" && d.name === "processData",
);
expect(funcDiff).toBeDefined();
expect(funcDiff?.type).toBe("modified");
expect(funcDiff?.impactLevel).toBe("breaking");
});
test("should detect removed functions", async () => {
const oldCode = `
export function oldFunction(): void {}
export function keepFunction(): void {}
`.trim();
const newCode = `
export function keepFunction(): void {}
`.trim();
const oldFile = join(tempDir, "old-removed.ts");
const newFile = join(tempDir, "new-removed.ts");
await fs.writeFile(oldFile, oldCode);
await fs.writeFile(newFile, newCode);
const oldAnalysis = await analyzer.analyzeFile(oldFile);
const newAnalysis = await analyzer.analyzeFile(newFile);
const diffs = await analyzer.detectDrift(oldAnalysis!, newAnalysis!);
const removedDiff = diffs.find((d) => d.name === "oldFunction");
expect(removedDiff).toBeDefined();
expect(removedDiff?.type).toBe("removed");
expect(removedDiff?.impactLevel).toBe("breaking");
});
test("should detect added functions", async () => {
const oldCode = `
export function existingFunction(): void {}
`.trim();
const newCode = `
export function existingFunction(): void {}
export function newFunction(): void {}
`.trim();
const oldFile = join(tempDir, "old-added.ts");
const newFile = join(tempDir, "new-added.ts");
await fs.writeFile(oldFile, oldCode);
await fs.writeFile(newFile, newCode);
const oldAnalysis = await analyzer.analyzeFile(oldFile);
const newAnalysis = await analyzer.analyzeFile(newFile);
const diffs = await analyzer.detectDrift(oldAnalysis!, newAnalysis!);
const addedDiff = diffs.find((d) => d.name === "newFunction");
expect(addedDiff).toBeDefined();
expect(addedDiff?.type).toBe("added");
expect(addedDiff?.impactLevel).toBe("patch");
});
test("should detect minor changes", async () => {
const oldCode = `
function internalFunction(): void {}
`.trim();
const newCode = `
export function internalFunction(): void {}
`.trim();
const oldFile = join(tempDir, "old-minor.ts");
const newFile = join(tempDir, "new-minor.ts");
await fs.writeFile(oldFile, oldCode);
await fs.writeFile(newFile, newCode);
const oldAnalysis = await analyzer.analyzeFile(oldFile);
const newAnalysis = await analyzer.analyzeFile(newFile);
const diffs = await analyzer.detectDrift(oldAnalysis!, newAnalysis!);
const minorDiff = diffs.find((d) => d.name === "internalFunction");
expect(minorDiff).toBeDefined();
expect(minorDiff?.type).toBe("modified");
expect(minorDiff?.impactLevel).toBe("minor");
});
});
describe("Edge Cases", () => {
test("should handle empty files", async () => {
const filePath = join(tempDir, "empty.ts");
await fs.writeFile(filePath, "");
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.functions).toHaveLength(0);
expect(result?.classes).toHaveLength(0);
});
test("should handle files with only comments", async () => {
const code = `
// This is a comment
/* Multi-line
comment */
`.trim();
const filePath = join(tempDir, "comments-only.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.functions).toHaveLength(0);
});
test("should handle syntax errors gracefully", async () => {
const code = `
export function broken(
// Missing closing paren and body
`.trim();
const filePath = join(tempDir, "syntax-error.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
// Should still return a result, even if incomplete
expect(result).not.toBeNull();
});
test("should return null for unsupported file types", async () => {
const filePath = join(tempDir, "test.txt");
await fs.writeFile(filePath, "Some text content");
const result = await analyzer.analyzeFile(filePath);
expect(result).toBeNull();
});
});
describe("Content Hashing", () => {
test("should generate consistent content hashes", async () => {
const code = `export function test(): void {}`;
const file1 = join(tempDir, "hash1.ts");
const file2 = join(tempDir, "hash2.ts");
await fs.writeFile(file1, code);
await fs.writeFile(file2, code);
const result1 = await analyzer.analyzeFile(file1);
const result2 = await analyzer.analyzeFile(file2);
expect(result1?.contentHash).toBe(result2?.contentHash);
});
test("should generate different hashes for different content", async () => {
const code1 = `export function test1(): void {}`;
const code2 = `export function test2(): void {}`;
const file1 = join(tempDir, "diff1.ts");
const file2 = join(tempDir, "diff2.ts");
await fs.writeFile(file1, code1);
await fs.writeFile(file2, code2);
const result1 = await analyzer.analyzeFile(file1);
const result2 = await analyzer.analyzeFile(file2);
expect(result1?.contentHash).not.toBe(result2?.contentHash);
});
});
describe("Multi-Language Support", () => {
test("should handle Python files with tree-sitter", async () => {
const pythonCode = `
def hello_world():
print("Hello, World!")
class MyClass:
def __init__(self):
self.value = 42
`.trim();
const filePath = join(tempDir, "test.py");
await fs.writeFile(filePath, pythonCode);
const result = await analyzer.analyzeFile(filePath);
expect(result).toBeDefined();
expect(result?.language).toBe("python");
expect(result?.filePath).toBe(filePath);
expect(result?.linesOfCode).toBeGreaterThan(0);
});
test("should handle Go files with tree-sitter", async () => {
const goCode = `
package main
func main() {
println("Hello, World!")
}
`.trim();
const filePath = join(tempDir, "test.go");
await fs.writeFile(filePath, goCode);
const result = await analyzer.analyzeFile(filePath);
expect(result).toBeDefined();
expect(result?.language).toBe("go");
});
test("should handle Rust files with tree-sitter", async () => {
const rustCode = `
fn main() {
println!("Hello, World!");
}
`.trim();
const filePath = join(tempDir, "test.rs");
await fs.writeFile(filePath, rustCode);
const result = await analyzer.analyzeFile(filePath);
expect(result).toBeDefined();
expect(result?.language).toBe("rust");
});
});
describe("Advanced TypeScript Features", () => {
test("should extract default values from parameters", async () => {
const code = `
export function withDefaults(
name: string = "default",
count: number = 42,
flag: boolean = true
): void {
console.log(name, count, flag);
}
`.trim();
const filePath = join(tempDir, "defaults.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const func = result?.functions.find((f) => f.name === "withDefaults");
expect(func).toBeDefined();
expect(func?.parameters.length).toBe(3);
const nameParam = func?.parameters.find((p) => p.name === "name");
expect(nameParam?.defaultValue).toBeTruthy();
});
test("should detect private methods with underscore prefix", async () => {
const code = `
export class TestClass {
public publicMethod(): void {}
private _privateMethod(): void {}
#reallyPrivate(): void {}
}
`.trim();
const filePath = join(tempDir, "private-methods.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const testClass = result?.classes[0];
expect(testClass).toBeDefined();
expect(testClass?.methods.length).toBeGreaterThanOrEqual(1);
});
test("should detect exported declarations correctly", async () => {
const code = `
export function exportedFunc(): void {}
function nonExportedFunc(): void {}
export const exportedConst = () => {};
const nonExportedConst = () => {};
`.trim();
const filePath = join(tempDir, "exports.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const exportedFunc = result?.functions.find(
(f) => f.name === "exportedFunc",
);
expect(exportedFunc?.isExported).toBe(true);
const exportedArrow = result?.functions.find(
(f) => f.name === "exportedConst",
);
expect(exportedArrow?.isExported).toBe(true);
});
test("should handle files without initialization", async () => {
const newAnalyzer = new ASTAnalyzer();
// Don't call initialize() - should auto-initialize
const code = `export function test(): void {}`;
const filePath = join(tempDir, "auto-init.ts");
await fs.writeFile(filePath, code);
const result = await newAnalyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.functions.length).toBeGreaterThan(0);
});
});
describe("Interface and Type Detection", () => {
test("should detect interface vs type differences", async () => {
const code = `
export interface UserInterface {
id: string;
name: string;
}
export type UserType = {
id: string;
name: string;
};
export type StatusType = "active" | "inactive";
`.trim();
const filePath = join(tempDir, "types-vs-interfaces.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
expect(result?.interfaces.length).toBe(1);
expect(result?.types.length).toBe(2);
const userInterface = result?.interfaces.find(
(i) => i.name === "UserInterface",
);
expect(userInterface?.isExported).toBe(true);
const statusType = result?.types.find((t) => t.name === "StatusType");
expect(statusType?.isExported).toBe(true);
});
test("should handle interface methods", async () => {
const code = `
export interface Repository {
save(data: string): Promise<void>;
load(): Promise<string>;
delete(id: string): boolean;
}
`.trim();
const filePath = join(tempDir, "interface-methods.ts");
await fs.writeFile(filePath, code);
const result = await analyzer.analyzeFile(filePath);
expect(result).not.toBeNull();
const repo = result?.interfaces.find((i) => i.name === "Repository");
expect(repo?.methods.length).toBe(3);
});
});
});
```