This is page 22 of 33. Use http://codebase.md/tosin2013/documcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
│ ├── agents
│ │ ├── documcp-ast.md
│ │ ├── documcp-deploy.md
│ │ ├── documcp-memory.md
│ │ ├── documcp-test.md
│ │ └── documcp-tool.md
│ ├── copilot-instructions.md
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── automated-changelog.md
│ │ ├── bug_report.md
│ │ ├── bug_report.yml
│ │ ├── documentation_issue.md
│ │ ├── feature_request.md
│ │ ├── feature_request.yml
│ │ ├── npm-publishing-fix.md
│ │ └── release_improvements.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── release-drafter.yml
│ └── workflows
│ ├── auto-merge.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── deploy-docs.yml
│ ├── README.md
│ ├── release-drafter.yml
│ └── release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .linkcheck.config.json
├── .markdown-link-check.json
├── .nvmrc
├── .pre-commit-config.yaml
├── .versionrc.json
├── ARCHITECTURAL_CHANGES_SUMMARY.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│ ├── .docusaurus
│ │ ├── docusaurus-plugin-content-docs
│ │ │ └── default
│ │ │ └── __mdx-loader-dependency.json
│ │ └── docusaurus-plugin-content-pages
│ │ └── default
│ │ └── __plugin.json
│ ├── adrs
│ │ ├── adr-0001-mcp-server-architecture.md
│ │ ├── adr-0002-repository-analysis-engine.md
│ │ ├── adr-0003-static-site-generator-recommendation-engine.md
│ │ ├── adr-0004-diataxis-framework-integration.md
│ │ ├── adr-0005-github-pages-deployment-automation.md
│ │ ├── adr-0006-mcp-tools-api-design.md
│ │ ├── adr-0007-mcp-prompts-and-resources-integration.md
│ │ ├── adr-0008-intelligent-content-population-engine.md
│ │ ├── adr-0009-content-accuracy-validation-framework.md
│ │ ├── adr-0010-mcp-resource-pattern-redesign.md
│ │ ├── adr-0011-ce-mcp-compatibility.md
│ │ ├── adr-0012-priority-scoring-system-for-documentation-drift.md
│ │ ├── adr-0013-release-pipeline-and-package-distribution.md
│ │ └── README.md
│ ├── api
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── hierarchy.js
│ │ │ ├── highlight.css
│ │ │ ├── icons.js
│ │ │ ├── icons.svg
│ │ │ ├── main.js
│ │ │ ├── navigation.js
│ │ │ ├── search.js
│ │ │ └── style.css
│ │ ├── hierarchy.html
│ │ ├── index.html
│ │ ├── modules.html
│ │ └── variables
│ │ └── TOOLS.html
│ ├── assets
│ │ └── logo.svg
│ ├── CE-MCP-FINDINGS.md
│ ├── development
│ │ └── MCP_INSPECTOR_TESTING.md
│ ├── docusaurus.config.js
│ ├── explanation
│ │ ├── architecture.md
│ │ └── index.md
│ ├── guides
│ │ ├── link-validation.md
│ │ ├── playwright-integration.md
│ │ └── playwright-testing-workflow.md
│ ├── how-to
│ │ ├── analytics-setup.md
│ │ ├── change-watcher.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── drift-priority-scoring.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── llm-integration.md
│ │ ├── local-testing.md
│ │ ├── performance-optimization.md
│ │ ├── prompting-guide.md
│ │ ├── repository-analysis.md
│ │ ├── seo-optimization.md
│ │ ├── site-monitoring.md
│ │ ├── troubleshooting.md
│ │ └── usage-examples.md
│ ├── index.md
│ ├── knowledge-graph.md
│ ├── package-lock.json
│ ├── package.json
│ ├── phase-2-intelligence.md
│ ├── reference
│ │ ├── api-overview.md
│ │ ├── cli.md
│ │ ├── configuration.md
│ │ ├── deploy-pages.md
│ │ ├── index.md
│ │ ├── mcp-tools.md
│ │ └── prompt-templates.md
│ ├── research
│ │ ├── cross-domain-integration
│ │ │ └── README.md
│ │ ├── domain-1-mcp-architecture
│ │ │ ├── index.md
│ │ │ └── mcp-performance-research.md
│ │ ├── domain-2-repository-analysis
│ │ │ └── README.md
│ │ ├── domain-3-ssg-recommendation
│ │ │ ├── index.md
│ │ │ └── ssg-performance-analysis.md
│ │ ├── domain-4-diataxis-integration
│ │ │ └── README.md
│ │ ├── domain-5-github-deployment
│ │ │ ├── github-pages-security-analysis.md
│ │ │ └── index.md
│ │ ├── domain-6-api-design
│ │ │ └── README.md
│ │ ├── README.md
│ │ ├── research-integration-summary-2025-01-14.md
│ │ ├── research-progress-template.md
│ │ └── research-questions-2025-01-14.md
│ ├── robots.txt
│ ├── sidebars.js
│ ├── sitemap.xml
│ ├── src
│ │ └── css
│ │ └── custom.css
│ └── tutorials
│ ├── development-setup.md
│ ├── environment-setup.md
│ ├── first-deployment.md
│ ├── getting-started.md
│ ├── index.md
│ ├── memory-workflows.md
│ └── user-onboarding.md
├── ISSUE_IMPLEMENTATION_SUMMARY.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│ └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│ ├── benchmarks
│ │ └── performance.ts
│ ├── index.ts
│ ├── memory
│ │ ├── contextual-retrieval.ts
│ │ ├── deployment-analytics.ts
│ │ ├── enhanced-manager.ts
│ │ ├── export-import.ts
│ │ ├── freshness-kg-integration.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kg-code-integration.ts
│ │ ├── kg-health.ts
│ │ ├── kg-integration.ts
│ │ ├── kg-link-validator.ts
│ │ ├── kg-storage.ts
│ │ ├── knowledge-graph.ts
│ │ ├── learning.ts
│ │ ├── manager.ts
│ │ ├── multi-agent-sharing.ts
│ │ ├── pruning.ts
│ │ ├── schemas.ts
│ │ ├── storage.ts
│ │ ├── temporal-analysis.ts
│ │ ├── user-preferences.ts
│ │ └── visualization.ts
│ ├── prompts
│ │ └── technical-writer-prompts.ts
│ ├── scripts
│ │ └── benchmark.ts
│ ├── templates
│ │ └── playwright
│ │ ├── accessibility.spec.template.ts
│ │ ├── Dockerfile.template
│ │ ├── docs-e2e.workflow.template.yml
│ │ ├── link-validation.spec.template.ts
│ │ └── playwright.config.template.ts
│ ├── tools
│ │ ├── analyze-deployments.ts
│ │ ├── analyze-readme.ts
│ │ ├── analyze-repository.ts
│ │ ├── change-watcher.ts
│ │ ├── check-documentation-links.ts
│ │ ├── cleanup-agent-artifacts.ts
│ │ ├── deploy-pages.ts
│ │ ├── detect-gaps.ts
│ │ ├── evaluate-readme-health.ts
│ │ ├── generate-config.ts
│ │ ├── generate-contextual-content.ts
│ │ ├── generate-llm-context.ts
│ │ ├── generate-readme-template.ts
│ │ ├── generate-technical-writer-prompts.ts
│ │ ├── kg-health-check.ts
│ │ ├── manage-preferences.ts
│ │ ├── manage-sitemap.ts
│ │ ├── optimize-readme.ts
│ │ ├── populate-content.ts
│ │ ├── readme-best-practices.ts
│ │ ├── recommend-ssg.ts
│ │ ├── setup-playwright-tests.ts
│ │ ├── setup-structure.ts
│ │ ├── simulate-execution.ts
│ │ ├── sync-code-to-docs.ts
│ │ ├── test-local-deployment.ts
│ │ ├── track-documentation-freshness.ts
│ │ ├── update-existing-documentation.ts
│ │ ├── validate-content.ts
│ │ ├── validate-documentation-freshness.ts
│ │ ├── validate-readme-checklist.ts
│ │ └── verify-deployment.ts
│ ├── types
│ │ └── api.ts
│ ├── utils
│ │ ├── artifact-detector.ts
│ │ ├── ast-analyzer.ts
│ │ ├── change-watcher.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── execution-simulator.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── llm-client.ts
│ │ ├── permission-checker.ts
│ │ ├── semantic-analyzer.ts
│ │ ├── sitemap-generator.ts
│ │ ├── usage-metadata.ts
│ │ └── user-feedback-integration.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── call-graph-builder.test.ts
│ ├── change-watcher-priority.integration.test.ts
│ ├── change-watcher.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── execution-simulator.test.ts
│ ├── functional
│ │ └── tools.test.ts
│ ├── integration
│ │ ├── kg-documentation-workflow.test.ts
│ │ ├── knowledge-graph-workflow.test.ts
│ │ ├── mcp-readme-tools.test.ts
│ │ ├── memory-mcp-tools.test.ts
│ │ ├── readme-technical-writer.test.ts
│ │ └── workflow.test.ts
│ ├── memory
│ │ ├── contextual-retrieval.test.ts
│ │ ├── enhanced-manager.test.ts
│ │ ├── export-import.test.ts
│ │ ├── freshness-kg-integration.test.ts
│ │ ├── kg-code-integration.test.ts
│ │ ├── kg-health.test.ts
│ │ ├── kg-link-validator.test.ts
│ │ ├── kg-storage-validation.test.ts
│ │ ├── kg-storage.test.ts
│ │ ├── knowledge-graph-documentation-examples.test.ts
│ │ ├── knowledge-graph-enhanced.test.ts
│ │ ├── knowledge-graph.test.ts
│ │ ├── learning.test.ts
│ │ ├── manager-advanced.test.ts
│ │ ├── manager.test.ts
│ │ ├── mcp-resource-integration.test.ts
│ │ ├── mcp-tool-persistence.test.ts
│ │ ├── schemas-documentation-examples.test.ts
│ │ ├── schemas.test.ts
│ │ ├── storage.test.ts
│ │ ├── temporal-analysis.test.ts
│ │ └── user-preferences.test.ts
│ ├── performance
│ │ ├── memory-load-testing.test.ts
│ │ └── memory-stress-testing.test.ts
│ ├── prompts
│ │ ├── guided-workflow-prompts.test.ts
│ │ └── technical-writer-prompts.test.ts
│ ├── server.test.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── all-tools.test.ts
│ │ ├── analyze-coverage.test.ts
│ │ ├── analyze-deployments.test.ts
│ │ ├── analyze-readme.test.ts
│ │ ├── analyze-repository.test.ts
│ │ ├── check-documentation-links.test.ts
│ │ ├── cleanup-agent-artifacts.test.ts
│ │ ├── deploy-pages-kg-retrieval.test.ts
│ │ ├── deploy-pages-tracking.test.ts
│ │ ├── deploy-pages.test.ts
│ │ ├── detect-gaps.test.ts
│ │ ├── evaluate-readme-health.test.ts
│ │ ├── generate-contextual-content.test.ts
│ │ ├── generate-llm-context.test.ts
│ │ ├── generate-readme-template.test.ts
│ │ ├── generate-technical-writer-prompts.test.ts
│ │ ├── kg-health-check.test.ts
│ │ ├── manage-sitemap.test.ts
│ │ ├── optimize-readme.test.ts
│ │ ├── readme-best-practices.test.ts
│ │ ├── recommend-ssg-historical.test.ts
│ │ ├── recommend-ssg-preferences.test.ts
│ │ ├── recommend-ssg.test.ts
│ │ ├── simple-coverage.test.ts
│ │ ├── sync-code-to-docs.test.ts
│ │ ├── test-local-deployment.test.ts
│ │ ├── tool-error-handling.test.ts
│ │ ├── track-documentation-freshness.test.ts
│ │ ├── validate-content.test.ts
│ │ ├── validate-documentation-freshness.test.ts
│ │ └── validate-readme-checklist.test.ts
│ ├── types
│ │ └── type-safety.test.ts
│ └── utils
│ ├── artifact-detector.test.ts
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector-diataxis.test.ts
│ ├── drift-detector-priority.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ ├── llm-client.test.ts
│ ├── semantic-analyzer.test.ts
│ ├── sitemap-generator.test.ts
│ ├── usage-metadata.test.ts
│ └── user-feedback-integration.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/tests/performance/memory-load-testing.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Memory System Performance and Load Testing
3 | * Tests performance, scalability, and resource usage of memory system
4 | * Part of Issue #57 - Memory System Performance and Load Testing
5 | */
6 |
7 | import { promises as fs } from "fs";
8 | import path from "path";
9 | import os from "os";
10 | import { performance } from "perf_hooks";
11 | import { MemoryManager } from "../../src/memory/manager.js";
12 | import { EnhancedMemoryManager } from "../../src/memory/enhanced-manager.js";
13 | import { IncrementalLearningSystem } from "../../src/memory/learning.js";
14 | import { KnowledgeGraph } from "../../src/memory/knowledge-graph.js";
15 | import {
16 | initializeMemory,
17 | rememberAnalysis,
18 | rememberRecommendation,
19 | getSimilarProjects,
20 | } from "../../src/memory/integration.js";
21 |
22 | interface PerformanceMetrics {
23 | operationTime: number;
24 | memoryUsed: number;
25 | operationsPerSecond: number;
26 | throughput: number;
27 | }
28 |
29 | describe("Memory System Performance and Load Testing", () => {
30 | let tempDir: string;
31 | let memoryManager: MemoryManager;
32 |
33 | beforeEach(async () => {
34 | tempDir = path.join(
35 | os.tmpdir(),
36 | `memory-performance-test-${Date.now()}-${Math.random()
37 | .toString(36)
38 | .substr(2, 9)}`,
39 | );
40 | await fs.mkdir(tempDir, { recursive: true });
41 |
42 | memoryManager = new MemoryManager(tempDir);
43 | await memoryManager.initialize();
44 | });
45 |
46 | afterEach(async () => {
47 | try {
48 | await fs.rm(tempDir, { recursive: true, force: true });
49 | } catch (error) {
50 | // Ignore cleanup errors
51 | }
52 | });
53 |
54 | function measurePerformance<T>(
55 | operation: () => Promise<T>,
56 | ): Promise<{ result: T; metrics: PerformanceMetrics }> {
57 | return new Promise(async (resolve) => {
58 | const startTime = performance.now();
59 | const startMemory = process.memoryUsage();
60 |
61 | const result = await operation();
62 |
63 | const endTime = performance.now();
64 | const endMemory = process.memoryUsage();
65 |
66 | const operationTime = endTime - startTime;
67 | const memoryUsed = endMemory.heapUsed - startMemory.heapUsed;
68 |
69 | resolve({
70 | result,
71 | metrics: {
72 | operationTime,
73 | memoryUsed,
74 | operationsPerSecond: 1000 / operationTime,
75 | throughput: 1000 / operationTime,
76 | },
77 | });
78 | });
79 | }
80 |
81 | describe("Basic Operations Performance", () => {
82 | test("should perform single memory operations efficiently", async () => {
83 | memoryManager.setContext({ projectId: "performance-single" });
84 |
85 | const testData = {
86 | projectId: "performance-single",
87 | language: { primary: "typescript" },
88 | framework: { name: "react" },
89 | stats: { files: 100, lines: 10000 },
90 | };
91 |
92 | const { metrics: createMetrics } = await measurePerformance(async () => {
93 | return await memoryManager.remember("analysis", testData);
94 | });
95 |
96 | const memoryId = (await memoryManager.remember("analysis", testData)).id;
97 |
98 | const { metrics: readMetrics } = await measurePerformance(async () => {
99 | return await memoryManager.recall(memoryId);
100 | });
101 |
102 | const { metrics: searchMetrics } = await measurePerformance(async () => {
103 | return await memoryManager.search({ projectId: "performance-single" });
104 | });
105 |
106 | // Performance expectations (adjust based on system capabilities)
107 | expect(createMetrics.operationTime).toBeLessThan(100); // 100ms
108 | expect(readMetrics.operationTime).toBeLessThan(50); // 50ms
109 | expect(searchMetrics.operationTime).toBeLessThan(100); // 100ms
110 |
111 | // Memory usage should be reasonable
112 | expect(createMetrics.memoryUsed).toBeLessThan(10 * 1024 * 1024); // 10MB
113 | expect(readMetrics.memoryUsed).toBeLessThan(1 * 1024 * 1024); // 1MB
114 |
115 | console.log("Single Operation Performance:");
116 | console.log(
117 | `Create: ${createMetrics.operationTime.toFixed(2)}ms, Memory: ${(
118 | createMetrics.memoryUsed / 1024
119 | ).toFixed(2)}KB`,
120 | );
121 | console.log(
122 | `Read: ${readMetrics.operationTime.toFixed(2)}ms, Memory: ${(
123 | readMetrics.memoryUsed / 1024
124 | ).toFixed(2)}KB`,
125 | );
126 | console.log(
127 | `Search: ${searchMetrics.operationTime.toFixed(2)}ms, Memory: ${(
128 | searchMetrics.memoryUsed / 1024
129 | ).toFixed(2)}KB`,
130 | );
131 | });
132 |
133 | test("should handle batch operations efficiently", async () => {
134 | memoryManager.setContext({ projectId: "performance-batch" });
135 |
136 | const batchSize = 100;
137 | const testData = Array.from({ length: batchSize }, (_, i) => ({
138 | projectId: "performance-batch",
139 | index: i,
140 | language: { primary: i % 2 === 0 ? "typescript" : "javascript" },
141 | framework: {
142 | name: i % 3 === 0 ? "react" : i % 3 === 1 ? "vue" : "angular",
143 | },
144 | stats: { files: 10 + i, lines: 1000 + i * 100 },
145 | }));
146 |
147 | const { metrics: batchCreateMetrics } = await measurePerformance(
148 | async () => {
149 | const promises = testData.map((data) =>
150 | memoryManager.remember("analysis", data),
151 | );
152 | return await Promise.all(promises);
153 | },
154 | );
155 |
156 | const { metrics: batchSearchMetrics } = await measurePerformance(
157 | async () => {
158 | return await memoryManager.search({ projectId: "performance-batch" });
159 | },
160 | );
161 |
162 | // Batch operations should be efficient
163 | expect(batchCreateMetrics.operationTime).toBeLessThan(5000); // 5 seconds for 100 items
164 | expect(batchSearchMetrics.operationTime).toBeLessThan(1000); // 1 second to search 100 items
165 |
166 | // Calculate throughput
167 | const createThroughput =
168 | batchSize / (batchCreateMetrics.operationTime / 1000);
169 | const searchThroughput =
170 | batchSize / (batchSearchMetrics.operationTime / 1000);
171 |
172 | expect(createThroughput).toBeGreaterThan(20); // At least 20 ops/sec
173 | expect(searchThroughput).toBeGreaterThan(100); // At least 100 searches/sec
174 |
175 | console.log("Batch Operation Performance:");
176 | console.log(
177 | `Create ${batchSize} items: ${batchCreateMetrics.operationTime.toFixed(
178 | 2,
179 | )}ms (${createThroughput.toFixed(2)} ops/sec)`,
180 | );
181 | console.log(
182 | `Search ${batchSize} items: ${batchSearchMetrics.operationTime.toFixed(
183 | 2,
184 | )}ms (${searchThroughput.toFixed(2)} ops/sec)`,
185 | );
186 | });
187 | });
188 |
189 | describe("Scalability Testing", () => {
190 | test("should scale linearly with data size", async () => {
191 | memoryManager.setContext({ projectId: "scalability-test" });
192 |
193 | const testSizes = [10, 50, 100, 500];
194 | const results: Array<{
195 | size: number;
196 | createTime: number;
197 | searchTime: number;
198 | }> = [];
199 |
200 | for (const size of testSizes) {
201 | const testData = Array.from({ length: size }, (_, i) => ({
202 | projectId: "scalability-test",
203 | index: i,
204 | data: `test-data-${i}`,
205 | timestamp: new Date().toISOString(),
206 | }));
207 |
208 | // Measure creation time
209 | const { metrics: createMetrics } = await measurePerformance(
210 | async () => {
211 | const promises = testData.map((data) =>
212 | memoryManager.remember("analysis", data),
213 | );
214 | return await Promise.all(promises);
215 | },
216 | );
217 |
218 | // Measure search time
219 | const { metrics: searchMetrics } = await measurePerformance(
220 | async () => {
221 | return await memoryManager.search({
222 | projectId: "scalability-test",
223 | });
224 | },
225 | );
226 |
227 | results.push({
228 | size,
229 | createTime: createMetrics.operationTime,
230 | searchTime: searchMetrics.operationTime,
231 | });
232 | }
233 |
234 | // Verify roughly linear scaling (allow for some variance)
235 | for (let i = 1; i < results.length; i++) {
236 | const prev = results[i - 1];
237 | const curr = results[i];
238 |
239 | const sizeRatio = curr.size / prev.size;
240 | const createTimeRatio = curr.createTime / prev.createTime;
241 | const searchTimeRatio = curr.searchTime / prev.searchTime;
242 |
243 | // Create time should scale roughly linearly (within 3x of size ratio)
244 | expect(createTimeRatio).toBeLessThan(sizeRatio * 3);
245 |
246 | // Search time should not degrade too badly (within 2x of size ratio)
247 | expect(searchTimeRatio).toBeLessThan(sizeRatio * 2);
248 | }
249 |
250 | console.log("Scalability Results:");
251 | results.forEach((result) => {
252 | console.log(
253 | `Size ${result.size}: Create ${result.createTime.toFixed(
254 | 2,
255 | )}ms, Search ${result.searchTime.toFixed(2)}ms`,
256 | );
257 | });
258 | });
259 |
260 | test("should handle large individual memories efficiently", async () => {
261 | memoryManager.setContext({ projectId: "large-memory-test" });
262 |
263 | const sizes = [
264 | { name: "small", data: "x".repeat(1000) }, // 1KB
265 | { name: "medium", data: "x".repeat(10000) }, // 10KB
266 | { name: "large", data: "x".repeat(100000) }, // 100KB
267 | { name: "xlarge", data: "x".repeat(1000000) }, // 1MB
268 | ];
269 |
270 | const results: Array<{
271 | name: string;
272 | createTime: number;
273 | readTime: number;
274 | }> = [];
275 |
276 | for (const size of sizes) {
277 | const testData = {
278 | projectId: "large-memory-test",
279 | size: size.name,
280 | content: size.data,
281 | metadata: { size: size.data.length },
282 | };
283 |
284 | // Measure creation time
285 | const { result: memory, metrics: createMetrics } =
286 | await measurePerformance(async () => {
287 | return await memoryManager.remember("analysis", testData);
288 | });
289 |
290 | // Measure read time
291 | const { metrics: readMetrics } = await measurePerformance(async () => {
292 | return await memoryManager.recall(memory.id);
293 | });
294 |
295 | results.push({
296 | name: size.name,
297 | createTime: createMetrics.operationTime,
298 | readTime: readMetrics.operationTime,
299 | });
300 |
301 | // Large memories should still be handled within reasonable time
302 | expect(createMetrics.operationTime).toBeLessThan(5000); // 5 seconds
303 | expect(readMetrics.operationTime).toBeLessThan(1000); // 1 second
304 | }
305 |
306 | console.log("Large Memory Performance:");
307 | results.forEach((result) => {
308 | console.log(
309 | `${result.name}: Create ${result.createTime.toFixed(
310 | 2,
311 | )}ms, Read ${result.readTime.toFixed(2)}ms`,
312 | );
313 | });
314 | });
315 | });
316 |
317 | describe("Concurrent Operations Performance", () => {
318 | test("should handle concurrent read/write operations", async () => {
319 | memoryManager.setContext({ projectId: "concurrent-test" });
320 |
321 | // Pre-populate with some data
322 | const initialData = Array.from({ length: 50 }, (_, i) => ({
323 | projectId: "concurrent-test",
324 | index: i,
325 | data: `initial-data-${i}`,
326 | }));
327 |
328 | const initialMemories = await Promise.all(
329 | initialData.map((data) => memoryManager.remember("analysis", data)),
330 | );
331 |
332 | const concurrentOperations = 20;
333 |
334 | const { metrics: concurrentMetrics } = await measurePerformance(
335 | async () => {
336 | const operations = Array.from(
337 | { length: concurrentOperations },
338 | async (_, i) => {
339 | if (i % 3 === 0) {
340 | // Create new memory
341 | return await memoryManager.remember("analysis", {
342 | projectId: "concurrent-test",
343 | index: 100 + i,
344 | data: `concurrent-data-${i}`,
345 | });
346 | } else if (i % 3 === 1) {
347 | // Read existing memory
348 | const randomMemory =
349 | initialMemories[
350 | Math.floor(Math.random() * initialMemories.length)
351 | ];
352 | return await memoryManager.recall(randomMemory.id);
353 | } else {
354 | // Search memories
355 | return await memoryManager.search({
356 | projectId: "concurrent-test",
357 | });
358 | }
359 | },
360 | );
361 |
362 | return await Promise.all(operations);
363 | },
364 | );
365 |
366 | expect(concurrentMetrics.operationTime).toBeLessThan(3000); // 3 seconds for 20 concurrent ops
367 |
368 | const throughput =
369 | concurrentOperations / (concurrentMetrics.operationTime / 1000);
370 | expect(throughput).toBeGreaterThan(5); // At least 5 concurrent ops/sec
371 |
372 | console.log("Concurrent Operations Performance:");
373 | console.log(
374 | `${concurrentOperations} concurrent ops: ${concurrentMetrics.operationTime.toFixed(
375 | 2,
376 | )}ms (${throughput.toFixed(2)} ops/sec)`,
377 | );
378 | });
379 |
380 | test("should maintain performance under sustained load", async () => {
381 | memoryManager.setContext({ projectId: "sustained-load-test" });
382 |
383 | const testDuration = 3000; // 3 seconds
384 | const operationInterval = 100; // Every 100ms
385 | const results: number[] = [];
386 |
387 | const startTime = Date.now();
388 | let operationCount = 0;
389 |
390 | while (Date.now() - startTime < testDuration) {
391 | const { metrics } = await measurePerformance(async () => {
392 | return await memoryManager.remember("analysis", {
393 | projectId: "sustained-load-test",
394 | index: operationCount++,
395 | timestamp: Date.now(),
396 | data: `sustained-load-data-${operationCount}`,
397 | });
398 | });
399 |
400 | results.push(metrics.operationTime);
401 |
402 | // Wait for next interval
403 | await new Promise((resolve) => setTimeout(resolve, operationInterval));
404 | }
405 |
406 | const avgTime =
407 | results.reduce((sum, time) => sum + time, 0) / results.length;
408 | const maxTime = Math.max(...results);
409 | const minTime = Math.min(...results);
410 |
411 | // Performance should remain consistent under sustained load
412 | expect(avgTime).toBeLessThan(200); // Average operation time < 200ms
413 | expect(maxTime).toBeLessThan(1000); // No single operation > 1 second
414 |
415 | // Performance degradation should be minimal
416 | const firstHalf = results.slice(0, Math.floor(results.length / 2));
417 | const secondHalf = results.slice(Math.floor(results.length / 2));
418 |
419 | const firstHalfAvg =
420 | firstHalf.reduce((sum, time) => sum + time, 0) / firstHalf.length;
421 | const secondHalfAvg =
422 | secondHalf.reduce((sum, time) => sum + time, 0) / secondHalf.length;
423 |
424 | const degradation = secondHalfAvg / firstHalfAvg;
425 | expect(degradation).toBeLessThan(2); // Less than 2x degradation
426 |
427 | console.log("Sustained Load Performance:");
428 | console.log(
429 | `Operations: ${results.length}, Avg: ${avgTime.toFixed(
430 | 2,
431 | )}ms, Min: ${minTime.toFixed(2)}ms, Max: ${maxTime.toFixed(2)}ms`,
432 | );
433 | console.log(
434 | `Performance degradation: ${((degradation - 1) * 100).toFixed(1)}%`,
435 | );
436 | });
437 | });
438 |
439 | describe("Memory Resource Usage", () => {
440 | test("should manage memory usage efficiently", async () => {
441 | memoryManager.setContext({ projectId: "memory-usage-test" });
442 |
443 | const initialMemory = process.memoryUsage();
444 | const memorySnapshots: Array<{ count: number; heapUsed: number }> = [];
445 |
446 | // Add memories in batches and monitor memory usage
447 | for (let batch = 0; batch < 10; batch++) {
448 | const batchSize = 100;
449 | const batchData = Array.from({ length: batchSize }, (_, i) => ({
450 | projectId: "memory-usage-test",
451 | batch,
452 | index: i,
453 | data: "x".repeat(1000), // 1KB per memory
454 | timestamp: new Date().toISOString(),
455 | }));
456 |
457 | await Promise.all(
458 | batchData.map((data) => memoryManager.remember("analysis", data)),
459 | );
460 |
461 | const currentMemory = process.memoryUsage();
462 | memorySnapshots.push({
463 | count: (batch + 1) * batchSize,
464 | heapUsed: currentMemory.heapUsed - initialMemory.heapUsed,
465 | });
466 |
467 | // Force garbage collection if available
468 | if (global.gc) {
469 | global.gc();
470 | }
471 | }
472 |
473 | // Memory usage should be reasonable
474 | const finalMemoryUsage = memorySnapshots[memorySnapshots.length - 1];
475 | const memoryPerItem = finalMemoryUsage.heapUsed / finalMemoryUsage.count;
476 |
477 | expect(memoryPerItem).toBeLessThan(50 * 1024); // Less than 50KB per memory item (including overhead)
478 | expect(finalMemoryUsage.heapUsed).toBeLessThan(100 * 1024 * 1024); // Less than 100MB total
479 |
480 | console.log("Memory Usage Analysis:");
481 | console.log(
482 | `Total items: ${finalMemoryUsage.count}, Total memory: ${(
483 | finalMemoryUsage.heapUsed /
484 | 1024 /
485 | 1024
486 | ).toFixed(2)}MB`,
487 | );
488 | console.log(`Memory per item: ${(memoryPerItem / 1024).toFixed(2)}KB`);
489 | });
490 |
491 | test("should not leak memory on cleanup operations", async () => {
492 | memoryManager.setContext({ projectId: "memory-leak-test" });
493 |
494 | const initialMemory = process.memoryUsage();
495 |
496 | // Create and delete memories multiple times
497 | for (let cycle = 0; cycle < 5; cycle++) {
498 | const memories = [];
499 |
500 | // Create memories
501 | for (let i = 0; i < 100; i++) {
502 | const memory = await memoryManager.remember("analysis", {
503 | projectId: "memory-leak-test",
504 | cycle,
505 | index: i,
506 | data: "x".repeat(1000),
507 | });
508 | memories.push(memory);
509 | }
510 |
511 | // Delete all memories
512 | for (const memory of memories) {
513 | await memoryManager.forget(memory.id);
514 | }
515 |
516 | // Force garbage collection
517 | if (global.gc) {
518 | global.gc();
519 | }
520 | }
521 |
522 | const finalMemory = process.memoryUsage();
523 | const memoryDifference = finalMemory.heapUsed - initialMemory.heapUsed;
524 |
525 | // Memory usage should return close to initial levels
526 | expect(memoryDifference).toBeLessThan(15 * 1024 * 1024); // Less than 15MB difference
527 |
528 | console.log("Memory Leak Test:");
529 | console.log(
530 | `Memory difference: ${(memoryDifference / 1024 / 1024).toFixed(2)}MB`,
531 | );
532 | });
533 | });
534 |
535 | describe("Enhanced Components Performance", () => {
536 | test("should benchmark enhanced memory manager performance", async () => {
537 | const enhancedTempDir = path.join(tempDir, "enhanced");
538 | await fs.mkdir(enhancedTempDir, { recursive: true });
539 |
540 | const enhancedManager = new EnhancedMemoryManager(enhancedTempDir);
541 | await enhancedManager.initialize();
542 |
543 | enhancedManager.setContext({ projectId: "enhanced-performance" });
544 |
545 | const projectFeatures: import("../../src/memory/learning.js").ProjectFeatures =
546 | {
547 | language: "typescript",
548 | framework: "react",
549 | size: "medium",
550 | complexity: "moderate",
551 | hasTests: true,
552 | hasCI: true,
553 | hasDocs: true,
554 | isOpenSource: true,
555 | };
556 |
557 | const baseRecommendation = {
558 | recommended: "docusaurus",
559 | confidence: 0.8,
560 | score: 0.85,
561 | };
562 |
563 | // Benchmark enhanced recommendation
564 | const { metrics: enhancedMetrics } = await measurePerformance(
565 | async () => {
566 | return await enhancedManager.getEnhancedRecommendation(
567 | "/test/enhanced-performance",
568 | baseRecommendation,
569 | projectFeatures,
570 | );
571 | },
572 | );
573 |
574 | expect(enhancedMetrics.operationTime).toBeLessThan(5000); // 5 seconds
575 |
576 | // Benchmark intelligent analysis
577 | const analysisData = {
578 | language: "typescript",
579 | framework: "react",
580 | size: "medium",
581 | hasTests: true,
582 | hasCI: true,
583 | };
584 |
585 | const { metrics: analysisMetrics } = await measurePerformance(
586 | async () => {
587 | return await enhancedManager.getIntelligentAnalysis(
588 | "/test/enhanced-performance",
589 | analysisData,
590 | );
591 | },
592 | );
593 |
594 | expect(analysisMetrics.operationTime).toBeLessThan(3000); // 3 seconds
595 |
596 | console.log("Enhanced Components Performance:");
597 | console.log(
598 | `Enhanced recommendation: ${enhancedMetrics.operationTime.toFixed(
599 | 2,
600 | )}ms`,
601 | );
602 | console.log(
603 | `Intelligent analysis: ${analysisMetrics.operationTime.toFixed(2)}ms`,
604 | );
605 | });
606 |
607 | test("should benchmark learning system performance", async () => {
608 | const learningTempDir = path.join(tempDir, "learning");
609 | await fs.mkdir(learningTempDir, { recursive: true });
610 |
611 | const tempLearningManager = new MemoryManager(learningTempDir);
612 | await tempLearningManager.initialize();
613 | const learningSystem = new IncrementalLearningSystem(tempLearningManager);
614 | await learningSystem.initialize();
615 |
616 | const projectFeatures: import("../../src/memory/learning.js").ProjectFeatures =
617 | {
618 | language: "python",
619 | framework: "django",
620 | size: "large",
621 | complexity: "complex",
622 | hasTests: true,
623 | hasCI: true,
624 | hasDocs: false,
625 | isOpenSource: true,
626 | };
627 |
628 | const baseRecommendation = {
629 | recommended: "sphinx",
630 | confidence: 0.7,
631 | };
632 |
633 | // Add training data through memory manager (learning system learns from stored memories)
634 | for (let i = 0; i < 50; i++) {
635 | await tempLearningManager.remember("analysis", {
636 | ...projectFeatures,
637 | index: i,
638 | feedback: {
639 | rating: 3 + (i % 3),
640 | helpful: i % 2 === 0,
641 | comments: `Training feedback ${i}`,
642 | },
643 | });
644 | }
645 |
646 | // Benchmark improved recommendation
647 | const { metrics: improveMetrics } = await measurePerformance(async () => {
648 | return await learningSystem.getImprovedRecommendation(
649 | projectFeatures,
650 | baseRecommendation,
651 | );
652 | });
653 |
654 | expect(improveMetrics.operationTime).toBeLessThan(1000); // 1 second
655 |
656 | // Benchmark pattern detection
657 | const { metrics: patternMetrics } = await measurePerformance(async () => {
658 | return await learningSystem.getPatterns();
659 | });
660 |
661 | expect(patternMetrics.operationTime).toBeLessThan(2000); // 2 seconds
662 |
663 | console.log("Learning System Performance:");
664 | console.log(
665 | `Improved recommendation: ${improveMetrics.operationTime.toFixed(2)}ms`,
666 | );
667 | console.log(
668 | `Pattern detection: ${patternMetrics.operationTime.toFixed(2)}ms`,
669 | );
670 | });
671 |
672 | test("should benchmark knowledge graph performance", async () => {
673 | const graphTempDir = path.join(tempDir, "graph");
674 | await fs.mkdir(graphTempDir, { recursive: true });
675 |
676 | const tempGraphManager = new MemoryManager(graphTempDir);
677 | await tempGraphManager.initialize();
678 | const knowledgeGraph = new KnowledgeGraph(tempGraphManager);
679 | await knowledgeGraph.initialize();
680 |
681 | // Add nodes and edges
682 | const nodeCount = 100;
683 | const edgeCount = 200;
684 |
685 | const { metrics: buildMetrics } = await measurePerformance(async () => {
686 | // Add nodes
687 | for (let i = 0; i < nodeCount; i++) {
688 | knowledgeGraph.addNode({
689 | id: `node-${i}`,
690 | type:
691 | i % 3 === 0 ? "project" : i % 3 === 1 ? "technology" : "pattern",
692 | label: `Node ${i}`,
693 | weight: 1.0,
694 | properties: {
695 | name: `Node ${i}`,
696 | category: i % 5 === 0 ? "frontend" : "backend",
697 | },
698 | });
699 | }
700 |
701 | // Add edges
702 | for (let i = 0; i < edgeCount; i++) {
703 | const sourceId = `node-${Math.floor(Math.random() * nodeCount)}`;
704 | const targetId = `node-${Math.floor(Math.random() * nodeCount)}`;
705 |
706 | if (sourceId !== targetId) {
707 | knowledgeGraph.addEdge({
708 | source: sourceId,
709 | target: targetId,
710 | type: i % 2 === 0 ? "uses" : "similar_to",
711 | weight: Math.random(),
712 | properties: {},
713 | confidence: Math.random(),
714 | });
715 | }
716 | }
717 | });
718 |
719 | expect(buildMetrics.operationTime).toBeLessThan(5000); // 5 seconds to build graph
720 |
721 | // Benchmark pathfinding
722 | const { metrics: pathMetrics } = await measurePerformance(async () => {
723 | return knowledgeGraph.findPath("node-0", "node-50");
724 | });
725 |
726 | expect(pathMetrics.operationTime).toBeLessThan(500); // 500ms for pathfinding
727 |
728 | // Benchmark node queries (using available methods)
729 | const { metrics: queryMetrics } = await measurePerformance(async () => {
730 | return knowledgeGraph.getAllNodes();
731 | });
732 |
733 | expect(queryMetrics.operationTime).toBeLessThan(1000); // 1 second for node queries
734 |
735 | console.log("Knowledge Graph Performance:");
736 | console.log(
737 | `Build graph (${nodeCount} nodes, ${edgeCount} edges): ${buildMetrics.operationTime.toFixed(
738 | 2,
739 | )}ms`,
740 | );
741 | console.log(`Find path: ${pathMetrics.operationTime.toFixed(2)}ms`);
742 | console.log(`Nodes query: ${queryMetrics.operationTime.toFixed(2)}ms`);
743 | });
744 | });
745 |
746 | describe("Integration Performance", () => {
747 | test("should benchmark MCP integration performance", async () => {
748 | // Test memory integration functions
749 | const analysisData = {
750 | projectId: "integration-performance",
751 | language: { primary: "go" },
752 | framework: { name: "gin" },
753 | stats: { files: 200, lines: 50000 },
754 | };
755 |
756 | const { metrics: analysisMetrics } = await measurePerformance(
757 | async () => {
758 | return await rememberAnalysis(
759 | "/test/integration-performance",
760 | analysisData,
761 | );
762 | },
763 | );
764 |
765 | const { metrics: recommendationMetrics } = await measurePerformance(
766 | async () => {
767 | return await rememberRecommendation("analysis-id", {
768 | recommended: "hugo",
769 | confidence: 0.9,
770 | });
771 | },
772 | );
773 |
774 | const { metrics: similarMetrics } = await measurePerformance(async () => {
775 | return await getSimilarProjects(analysisData, 5);
776 | });
777 |
778 | expect(analysisMetrics.operationTime).toBeLessThan(1000); // 1 second
779 | expect(recommendationMetrics.operationTime).toBeLessThan(1000); // 1 second
780 | expect(similarMetrics.operationTime).toBeLessThan(2000); // 2 seconds
781 |
782 | console.log("MCP Integration Performance:");
783 | console.log(
784 | `Remember analysis: ${analysisMetrics.operationTime.toFixed(2)}ms`,
785 | );
786 | console.log(
787 | `Remember recommendation: ${recommendationMetrics.operationTime.toFixed(
788 | 2,
789 | )}ms`,
790 | );
791 | console.log(
792 | `Get similar projects: ${similarMetrics.operationTime.toFixed(2)}ms`,
793 | );
794 | });
795 | });
796 | });
797 |
```
--------------------------------------------------------------------------------
/docs/adrs/adr-0006-mcp-tools-api-design.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | id: adr-6-mcp-tools-api-design
3 | title: "ADR-006: MCP Tools API Design and Interface Specification"
4 | sidebar_label: "ADR-006: MCP Tools API Design"
5 | sidebar_position: 6
6 | documcp:
7 | last_updated: "2025-01-14T00:00:00.000Z"
8 | last_validated: "2025-01-14T00:00:00.000Z"
9 | auto_updated: false
10 | update_frequency: monthly
11 | validated_against_commit: 210a274
12 | ---
13 |
14 | # ADR-006: MCP Tools API Design and Interface Specification
15 |
16 | ## Status
17 |
18 | Accepted
19 |
20 | ## Context
21 |
22 | DocuMCP must expose its functionality through a carefully designed set of MCP tools that provide comprehensive coverage of the documentation deployment workflow while maintaining clear separation of concerns, appropriate granularity, and excellent developer experience for MCP-enabled clients.
23 |
24 | The MCP Tools API serves as the primary interface between DocuMCP's intelligence and client applications like GitHub Copilot, Claude Desktop, and other MCP-enabled development environments. This API must balance several competing concerns:
25 |
26 | **Functional Requirements:**
27 |
28 | - Comprehensive repository analysis capabilities
29 | - Intelligent SSG recommendation with detailed justifications
30 | - Automated configuration generation for multiple SSGs
31 | - Diataxis-compliant documentation structure creation
32 | - GitHub Pages deployment workflow generation
33 | - Git integration for seamless deployment
34 |
35 | **Usability Requirements:**
36 |
37 | - Intuitive tool names and parameter structures
38 | - Comprehensive input validation with clear error messages
39 | - Consistent response formats across all tools
40 | - Rich metadata for client presentation and user guidance
41 | - Progressive disclosure of complexity (simple to advanced use cases)
42 |
43 | **Technical Requirements:**
44 |
45 | - Full MCP specification compliance
46 | - Robust error handling and recovery
47 | - Efficient parameter validation and sanitization
48 | - Scalable architecture supporting complex multi-step workflows
49 | - Extensible design for future functionality additions
50 |
51 | ## Decision
52 |
53 | We will implement a comprehensive MCP Tools API consisting of six core tools that cover the complete documentation deployment workflow, with additional utility tools for advanced scenarios and troubleshooting.
54 |
55 | ### Core MCP Tools Architecture:
56 |
57 | #### 1. Repository Analysis Tool (`analyzeRepository`)
58 |
59 | **Purpose**: Comprehensive repository analysis and project characterization
60 | **Scope**: Deep analysis of project structure, language ecosystems, existing documentation, and complexity assessment
61 |
62 | #### 2. SSG Recommendation Tool (`recommendSSG`)
63 |
64 | **Purpose**: Intelligent static site generator recommendation with detailed justifications
65 | **Scope**: Multi-criteria decision analysis with confidence scoring and alternative options
66 |
67 | #### 3. Configuration Generation Tool (`generateConfiguration`)
68 |
69 | **Purpose**: Create customized SSG configuration files and directory structures
70 | **Scope**: Template-based generation with project-specific customizations and validation
71 |
72 | #### 4. Diataxis Structure Tool (`createDiataxisStructure`)
73 |
74 | **Purpose**: Generate comprehensive Diataxis-compliant documentation frameworks
75 | **Scope**: Information architecture generation with content planning and navigation design
76 |
77 | #### 5. Deployment Workflow Tool (`generateWorkflow`)
78 |
79 | **Purpose**: Create optimized GitHub Actions workflows for automated deployment
80 | **Scope**: SSG-specific workflow generation with security best practices and performance optimization
81 |
82 | #### 6. Git Integration Tool (`generateGitCommands`)
83 |
84 | **Purpose**: Provide ready-to-execute Git commands for deployment and maintenance
85 | **Scope**: Context-aware command generation with branch management and deployment verification
86 |
87 | ### Supporting Tools:
88 |
89 | - `validateConfiguration`: Validate generated configurations and identify issues
90 | - `troubleshootDeployment`: Analyze deployment failures and provide remediation guidance
91 | - `optimizePerformance`: Analyze and optimize existing documentation site performance
92 | - `migrateDocumentation`: Assist with migration between different SSGs or frameworks
93 | - `cleanupAgentArtifacts`: Detect, classify, and clean up artifacts generated by AI coding agents (Phase 3)
94 |
95 | ## Alternatives Considered
96 |
97 | ### Monolithic Single Tool Approach
98 |
99 | - **Pros**: Simpler API surface, single entry point, easier client integration
100 | - **Cons**: Complex parameter structures, poor separation of concerns, difficult error handling
101 | - **Decision**: Rejected due to poor usability and maintainability
102 |
103 | ### Micro-Tool Architecture (15+ Small Tools)
104 |
105 | - **Pros**: Maximum granularity, precise control, composable workflows
106 | - **Cons**: Complex orchestration, cognitive overhead, fragmented user experience
107 | - **Decision**: Rejected due to complexity and poor user experience
108 |
109 | ### Stateful Session-Based API
110 |
111 | - **Pros**: Could maintain context across tool calls, simplified parameter passing
112 | - **Cons**: Session management complexity, state synchronization issues, harder client integration
113 | - **Decision**: Rejected to maintain MCP stateless principles
114 |
115 | ### External API Integration (REST/GraphQL)
116 |
117 | - **Pros**: Standard web technologies, extensive tooling ecosystem
118 | - **Cons**: Not MCP-compliant, additional infrastructure requirements, authentication complexity
119 | - **Decision**: Rejected due to MCP specification requirements
120 |
121 | ## Consequences
122 |
123 | ### Positive
124 |
125 | - **Clear Separation of Concerns**: Each tool has well-defined responsibility and scope
126 | - **Progressive Complexity**: Users can start simple and add sophistication as needed
127 | - **Excellent Error Handling**: Tool-specific validation and error reporting
128 | - **Client-Friendly**: Rich metadata and consistent response formats enhance client UX
129 | - **Extensible Architecture**: Easy to add new tools without breaking existing functionality
130 |
131 | ### Negative
132 |
133 | - **API Surface Complexity**: Six core tools plus supporting tools require comprehensive documentation
134 | - **Inter-Tool Coordination**: Some workflows require multiple tool calls with parameter passing
135 | - **Validation Overhead**: Each tool requires comprehensive input validation and error handling
136 |
137 | ### Risks and Mitigations
138 |
139 | - **API Complexity**: Provide comprehensive documentation and usage examples
140 | - **Parameter Evolution**: Use versioned schemas with backward compatibility
141 | - **Client Integration**: Offer reference implementations and integration guides
142 |
143 | ## Implementation Details
144 |
145 | ### Tool Parameter Schemas
146 |
147 | ```typescript
148 | // Core tool parameter interfaces
149 | interface AnalyzeRepositoryParams {
150 | repositoryPath: string;
151 | analysisDepth?: "basic" | "comprehensive" | "deep";
152 | focusAreas?: ("structure" | "languages" | "documentation" | "complexity")[];
153 | excludePatterns?: string[];
154 | }
155 |
156 | interface RecommendSSGParams {
157 | projectAnalysis: ProjectAnalysis;
158 | teamCapabilities?: TeamCapabilities;
159 | performanceRequirements?: PerformanceRequirements;
160 | customizationNeeds?: CustomizationNeeds;
161 | existingConstraints?: ProjectConstraints;
162 | }
163 |
164 | interface GenerateConfigurationParams {
165 | selectedSSG: SSGType;
166 | projectAnalysis: ProjectAnalysis;
167 | customizations?: SSGCustomizations;
168 | deploymentTarget?: DeploymentTarget;
169 | advancedOptions?: AdvancedConfigOptions;
170 | }
171 |
172 | interface CreateDiataxisStructureParams {
173 | selectedSSG: SSGType;
174 | projectType: ProjectType;
175 | existingContent?: ExistingContentAnalysis;
176 | contentComplexity?: "minimal" | "standard" | "comprehensive";
177 | navigationPreferences?: NavigationPreferences;
178 | }
179 |
180 | interface GenerateWorkflowParams {
181 | ssgType: SSGType;
182 | deploymentStrategy: "github-actions" | "branch-based" | "hybrid";
183 | securityRequirements?: SecurityRequirements;
184 | performanceOptimizations?: PerformanceOptions;
185 | environmentConfiguration?: EnvironmentConfig;
186 | }
187 |
188 | interface GenerateGitCommandsParams {
189 | deploymentStrategy: DeploymentStrategy;
190 | repositoryState: RepositoryState;
191 | branchConfiguration: BranchConfiguration;
192 | commitPreferences?: CommitPreferences;
193 | }
194 | ```
195 |
196 | ### Response Format Standardization
197 |
198 | ```typescript
199 | // Standardized response structure for all tools
200 | interface MCPToolResponse<T> {
201 | success: boolean;
202 | data?: T;
203 | error?: ErrorDetails;
204 | metadata: ResponseMetadata;
205 | recommendations?: Recommendation[];
206 | nextSteps?: NextStep[];
207 | }
208 |
209 | interface ResponseMetadata {
210 | toolVersion: string;
211 | executionTime: number;
212 | confidenceScore?: number;
213 | analysisDepth: string;
214 | timestamp: string;
215 | correlationId: string;
216 | }
217 |
218 | interface ErrorDetails {
219 | code: string;
220 | message: string;
221 | details: string;
222 | resolution?: string;
223 | documentation?: string;
224 | }
225 |
226 | interface Recommendation {
227 | type: "optimization" | "alternative" | "enhancement";
228 | priority: "low" | "medium" | "high";
229 | description: string;
230 | implementation?: string;
231 | resources?: string[];
232 | }
233 |
234 | interface NextStep {
235 | action: string;
236 | description: string;
237 | toolRequired?: string;
238 | parameters?: Record<string, any>;
239 | estimated_time?: string;
240 | }
241 | ```
242 |
243 | ### analyzeRepository Tool Implementation
244 |
245 | ```typescript
246 | const analyzeRepositoryTool: MCPTool = {
247 | name: "analyzeRepository",
248 | description: "Comprehensive repository analysis for documentation planning",
249 | inputSchema: {
250 | type: "object",
251 | properties: {
252 | repositoryPath: {
253 | type: "string",
254 | description: "Path to the repository to analyze",
255 | },
256 | analysisDepth: {
257 | type: "string",
258 | enum: ["basic", "comprehensive", "deep"],
259 | default: "comprehensive",
260 | description: "Depth of analysis to perform",
261 | },
262 | focusAreas: {
263 | type: "array",
264 | items: {
265 | type: "string",
266 | enum: ["structure", "languages", "documentation", "complexity"],
267 | },
268 | description: "Specific areas to focus analysis on",
269 | },
270 | excludePatterns: {
271 | type: "array",
272 | items: { type: "string" },
273 | description: "File patterns to exclude from analysis",
274 | },
275 | },
276 | required: ["repositoryPath"],
277 | },
278 | };
279 |
280 | async function handleAnalyzeRepository(
281 | params: AnalyzeRepositoryParams,
282 | ): Promise<MCPToolResponse<RepositoryAnalysis>> {
283 | try {
284 | const analysis = await repositoryAnalyzer.analyze(params);
285 |
286 | return {
287 | success: true,
288 | data: analysis,
289 | metadata: {
290 | toolVersion: "1.0.0",
291 | executionTime: analysis.executionTime,
292 | analysisDepth: params.analysisDepth || "comprehensive",
293 | timestamp: new Date().toISOString(),
294 | correlationId: generateCorrelationId(),
295 | },
296 | recommendations: generateAnalysisRecommendations(analysis),
297 | nextSteps: [
298 | {
299 | action: "Get SSG Recommendation",
300 | description:
301 | "Use analysis results to get intelligent SSG recommendations",
302 | toolRequired: "recommendSSG",
303 | parameters: { projectAnalysis: analysis },
304 | estimated_time: "< 1 minute",
305 | },
306 | ],
307 | };
308 | } catch (error) {
309 | return {
310 | success: false,
311 | error: {
312 | code: "ANALYSIS_FAILED",
313 | message: "Repository analysis failed",
314 | details: error.message,
315 | resolution: "Verify repository path and permissions",
316 | documentation: "https://documcp.dev/troubleshooting#analysis-errors",
317 | },
318 | metadata: {
319 | toolVersion: "1.0.0",
320 | executionTime: 0,
321 | analysisDepth: params.analysisDepth || "comprehensive",
322 | timestamp: new Date().toISOString(),
323 | correlationId: generateCorrelationId(),
324 | },
325 | };
326 | }
327 | }
328 | ```
329 |
330 | ### recommendSSG Tool Implementation
331 |
332 | ```typescript
333 | const recommendSSGTool: MCPTool = {
334 | name: "recommendSSG",
335 | description:
336 | "Intelligent static site generator recommendation with detailed justifications",
337 | inputSchema: {
338 | type: "object",
339 | properties: {
340 | projectAnalysis: {
341 | type: "object",
342 | description: "Repository analysis results from analyzeRepository tool",
343 | },
344 | teamCapabilities: {
345 | type: "object",
346 | properties: {
347 | technicalSkills: { type: "array", items: { type: "string" } },
348 | maintenanceCapacity: {
349 | type: "string",
350 | enum: ["minimal", "moderate", "extensive"],
351 | },
352 | learningAppetite: { type: "string", enum: ["low", "medium", "high"] },
353 | },
354 | },
355 | performanceRequirements: {
356 | type: "object",
357 | properties: {
358 | buildTimeImportance: {
359 | type: "string",
360 | enum: ["low", "medium", "high"],
361 | },
362 | siteSpeedPriority: {
363 | type: "string",
364 | enum: ["standard", "fast", "ultra-fast"],
365 | },
366 | scalabilityNeeds: {
367 | type: "string",
368 | enum: ["small", "medium", "large", "enterprise"],
369 | },
370 | },
371 | },
372 | },
373 | required: ["projectAnalysis"],
374 | },
375 | };
376 |
377 | async function handleRecommendSSG(
378 | params: RecommendSSGParams,
379 | ): Promise<MCPToolResponse<SSGRecommendation>> {
380 | try {
381 | const recommendation = await ssgRecommendationEngine.analyze(params);
382 |
383 | return {
384 | success: true,
385 | data: recommendation,
386 | metadata: {
387 | toolVersion: "1.0.0",
388 | executionTime: recommendation.analysisTime,
389 | confidenceScore: recommendation.confidence,
390 | analysisDepth: "comprehensive",
391 | timestamp: new Date().toISOString(),
392 | correlationId: generateCorrelationId(),
393 | },
394 | recommendations: [
395 | {
396 | type: "optimization",
397 | priority: "medium",
398 | description: "Consider performance optimization strategies",
399 | implementation: "Review build caching and incremental build options",
400 | },
401 | ],
402 | nextSteps: [
403 | {
404 | action: "Generate Configuration",
405 | description: "Create customized configuration for recommended SSG",
406 | toolRequired: "generateConfiguration",
407 | parameters: {
408 | selectedSSG: recommendation.primaryRecommendation.ssg,
409 | projectAnalysis: params.projectAnalysis,
410 | },
411 | estimated_time: "2-3 minutes",
412 | },
413 | ],
414 | };
415 | } catch (error) {
416 | console.error("SSG recommendation analysis failed:", error);
417 | return {
418 | success: false,
419 | error: {
420 | code: "SSG_RECOMMENDATION_FAILED",
421 | message: `Failed to analyze SSG recommendations: ${
422 | error instanceof Error ? error.message : "Unknown error"
423 | }`,
424 | resolution:
425 | "Check project analysis data and retry with valid parameters",
426 | },
427 | metadata: {
428 | toolVersion: "1.0.0",
429 | timestamp: new Date().toISOString(),
430 | correlationId: generateCorrelationId(),
431 | },
432 | };
433 | }
434 | }
435 | ```
436 |
437 | ### Input Validation System
438 |
439 | ```typescript
440 | interface ValidationRule {
441 | field: string;
442 | validator: (value: any) => ValidationResult;
443 | required: boolean;
444 | errorMessage: string;
445 | }
446 |
447 | class MCPToolValidator {
448 | validateParameters<T>(params: T, schema: JSONSchema): ValidationResult {
449 | const results = this.runSchemaValidation(params, schema);
450 | const semanticResults = this.runSemanticValidation(params);
451 |
452 | return this.combineValidationResults(results, semanticResults);
453 | }
454 |
455 | private runSemanticValidation(params: any): ValidationResult {
456 | const issues: ValidationIssue[] = [];
457 |
458 | // Repository path validation
459 | if (
460 | params.repositoryPath &&
461 | !this.isValidRepositoryPath(params.repositoryPath)
462 | ) {
463 | issues.push({
464 | field: "repositoryPath",
465 | message: "Repository path does not exist or is not accessible",
466 | severity: "error",
467 | resolution: "Verify the path exists and you have read permissions",
468 | });
469 | }
470 |
471 | // Cross-parameter validation
472 | if (params.analysisDepth === "deep" && params.focusAreas?.length > 2) {
473 | issues.push({
474 | field: "analysisDepth",
475 | message: "Deep analysis with multiple focus areas may be slow",
476 | severity: "warning",
477 | resolution:
478 | "Consider using comprehensive analysis or fewer focus areas",
479 | });
480 | }
481 |
482 | return { valid: issues.length === 0, issues };
483 | }
484 | }
485 | ```
486 |
487 | ## Tool Orchestration Patterns
488 |
489 | ### Sequential Workflow Pattern
490 |
491 | ```typescript
492 | // Common workflow: Analysis → Recommendation → Configuration → Deployment
493 | class DocumentationWorkflow {
494 | async executeCompleteWorkflow(
495 | repositoryPath: string,
496 | ): Promise<WorkflowResult> {
497 | try {
498 | // Step 1: Analyze repository
499 | const analysisResult = await this.callTool("analyzeRepository", {
500 | repositoryPath,
501 | });
502 | if (!analysisResult.success) {
503 | throw new Error(`Analysis failed: ${analysisResult.error?.message}`);
504 | }
505 |
506 | // Step 2: Get SSG recommendation
507 | const recommendationResult = await this.callTool("recommendSSG", {
508 | projectAnalysis: analysisResult.data,
509 | });
510 | if (!recommendationResult.success) {
511 | throw new Error(
512 | `Recommendation failed: ${recommendationResult.error?.message}`,
513 | );
514 | }
515 |
516 | // Step 3: Generate configuration
517 | const configResult = await this.callTool("generateConfiguration", {
518 | selectedSSG: recommendationResult.data.primaryRecommendation.ssg,
519 | projectAnalysis: analysisResult.data,
520 | });
521 | if (!configResult.success) {
522 | throw new Error(
523 | `Configuration generation failed: ${configResult.error?.message}`,
524 | );
525 | }
526 |
527 | // Step 4: Create Diataxis structure
528 | const structureResult = await this.callTool("createDiataxisStructure", {
529 | selectedSSG: recommendationResult.data.primaryRecommendation.ssg,
530 | projectType: analysisResult.data.projectType,
531 | });
532 | if (!structureResult.success) {
533 | console.warn(
534 | `Diataxis structure creation failed: ${structureResult.error?.message}`,
535 | );
536 | }
537 |
538 | // Step 5: Generate deployment workflow
539 | const workflowResult = await this.callTool("generateWorkflow", {
540 | ssgType: recommendationResult.data.primaryRecommendation.ssg,
541 | deploymentStrategy: "github-actions",
542 | });
543 | if (!workflowResult.success) {
544 | console.warn(
545 | `Workflow generation failed: ${workflowResult.error?.message}`,
546 | );
547 | }
548 |
549 | return this.combineResults([
550 | analysisResult,
551 | recommendationResult,
552 | configResult,
553 | structureResult,
554 | workflowResult,
555 | ]);
556 | } catch (error) {
557 | throw new Error(`Complete workflow failed: ${error.message}`);
558 | }
559 | }
560 | }
561 | ```
562 |
563 | ## Error Handling and Recovery
564 |
565 | ### Comprehensive Error Classification
566 |
567 | ```typescript
568 | enum ErrorCategory {
569 | VALIDATION = "validation",
570 | FILESYSTEM = "filesystem",
571 | ANALYSIS = "analysis",
572 | GENERATION = "generation",
573 | CONFIGURATION = "configuration",
574 | DEPLOYMENT = "deployment",
575 | NETWORK = "network",
576 | PERMISSION = "permission",
577 | }
578 |
579 | interface ErrorContext {
580 | tool: string;
581 | operation: string;
582 | parameters: Record<string, any>;
583 | environment: EnvironmentInfo;
584 | }
585 |
586 | class MCPErrorHandler {
587 | handleError(error: Error, context: ErrorContext): MCPToolResponse<null> {
588 | const classification = this.classifyError(error);
589 | const resolution = this.generateResolution(classification, context);
590 |
591 | return {
592 | success: false,
593 | error: {
594 | code: this.generateErrorCode(classification),
595 | message: this.formatUserMessage(error, classification),
596 | details: error.message,
597 | resolution: resolution.guidance,
598 | documentation: resolution.documentationUrl,
599 | },
600 | metadata: this.generateErrorMetadata(context),
601 | nextSteps: resolution.suggestedActions,
602 | };
603 | }
604 |
605 | private generateResolution(
606 | classification: ErrorClassification,
607 | context: ErrorContext,
608 | ): ErrorResolution {
609 | switch (classification.category) {
610 | case ErrorCategory.FILESYSTEM:
611 | return {
612 | guidance: "Verify file paths and permissions",
613 | documentationUrl:
614 | "https://documcp.dev/troubleshooting#filesystem-errors",
615 | suggestedActions: [
616 | {
617 | action: "Check file exists",
618 | description: `Verify ${context.parameters.repositoryPath} exists`,
619 | },
620 | {
621 | action: "Check permissions",
622 | description: "Ensure read access to repository directory",
623 | },
624 | ],
625 | };
626 | // ... other error categories
627 | }
628 | }
629 | }
630 | ```
631 |
632 | ## Performance Optimization
633 |
634 | ### Response Caching Strategy
635 |
636 | ```typescript
637 | interface CacheConfiguration {
638 | analyzeRepository: {
639 | ttl: 300;
640 | keyFields: ["repositoryPath", "analysisDepth"];
641 | };
642 | recommendSSG: { ttl: 3600; keyFields: ["projectAnalysis.signature"] };
643 | generateConfiguration: {
644 | ttl: 1800;
645 | keyFields: ["selectedSSG", "projectAnalysis.signature"];
646 | };
647 | }
648 |
649 | class MCPToolCache {
650 | async getCachedResponse<T>(
651 | toolName: string,
652 | parameters: any,
653 | ): Promise<MCPToolResponse<T> | null> {
654 | const cacheKey = this.generateCacheKey(toolName, parameters);
655 | const cached = await this.cache.get(cacheKey);
656 |
657 | if (cached && !this.isExpired(cached)) {
658 | return {
659 | ...cached,
660 | metadata: {
661 | ...cached.metadata,
662 | fromCache: true,
663 | cacheAge: Date.now() - cached.metadata.timestamp,
664 | },
665 | };
666 | }
667 |
668 | return null;
669 | }
670 | }
671 | ```
672 |
673 | ## Testing Strategy
674 |
675 | ### Tool Testing Framework
676 |
677 | ```typescript
678 | describe("MCP Tools API", () => {
679 | describe("analyzeRepository", () => {
680 | it("should analyze JavaScript project correctly");
681 | it("should handle missing repository gracefully");
682 | it("should respect analysis depth parameters");
683 | it("should exclude specified patterns");
684 | });
685 |
686 | describe("recommendSSG", () => {
687 | it("should recommend Hugo for large documentation sites");
688 | it("should recommend Jekyll for GitHub Pages simple sites");
689 | it("should provide confidence scores for all recommendations");
690 | it("should handle incomplete project analysis");
691 | });
692 |
693 | describe("Tool Integration", () => {
694 | it("should support complete workflow from analysis to deployment");
695 | it("should maintain parameter consistency across tool calls");
696 | it("should provide appropriate next steps guidance");
697 | });
698 | });
699 | ```
700 |
701 | ### Integration Testing
702 |
703 | ```typescript
704 | class MCPToolIntegrationTests {
705 | async testCompleteWorkflow(): Promise<void> {
706 | const testRepo = await this.createTestRepository();
707 |
708 | // Test full workflow
709 | const analysis = await this.callTool("analyzeRepository", {
710 | repositoryPath: testRepo,
711 | });
712 | expect(analysis.success).toBe(true);
713 |
714 | const recommendation = await this.callTool("recommendSSG", {
715 | projectAnalysis: analysis.data,
716 | });
717 | expect(recommendation.success).toBe(true);
718 | expect(recommendation.data.primaryRecommendation).toBeDefined();
719 |
720 | const config = await this.callTool("generateConfiguration", {
721 | selectedSSG: recommendation.data.primaryRecommendation.ssg,
722 | projectAnalysis: analysis.data,
723 | });
724 | expect(config.success).toBe(true);
725 |
726 | // Validate generated configuration
727 | await this.validateGeneratedFiles(config.data.files);
728 | }
729 | }
730 | ```
731 |
732 | ## Documentation and Examples
733 |
734 | ### Tool Usage Examples
735 |
736 | ```typescript
737 | // Example: Complete documentation setup workflow
738 | const examples = {
739 | basicSetup: {
740 | description: "Basic documentation setup for a JavaScript project",
741 | steps: [
742 | {
743 | tool: "analyzeRepository",
744 | parameters: { repositoryPath: "./my-project" },
745 | expectedResult: "Project analysis with language ecosystem detection",
746 | },
747 | {
748 | tool: "recommendSSG",
749 | parameters: { projectAnalysis: "${analysis_result}" },
750 | expectedResult: "SSG recommendation with justification",
751 | },
752 | ],
753 | },
754 | advancedSetup: {
755 | description: "Advanced setup with custom requirements",
756 | steps: [
757 | // ... detailed workflow steps
758 | ],
759 | },
760 | };
761 | ```
762 |
763 | ## Code Execution with MCP (CE-MCP) Compatibility (2025-12-09)
764 |
765 | ### Tool Design Validation
766 |
767 | **Research Findings**: Our tool API design is fully compatible with Code Mode clients:
768 |
769 | 1. **Composable Tools**: Each tool is independent and can be orchestrated via generated code
770 | 2. **Clear Separation**: Well-defined responsibilities enable clean code generation
771 | 3. **Zod Validation**: Provides excellent type information for LLM code generation
772 |
773 | ### Best Practices for Code Mode
774 |
775 | **Tool Description Optimization**:
776 |
777 | ```typescript
778 | // ✅ GOOD: Concise, focused descriptions
779 | {
780 | name: "analyze_repository",
781 | description: "Analyze project structure, languages, and documentation",
782 | // ~60 tokens, clear purpose
783 | }
784 |
785 | // ❌ AVOID: Verbose descriptions that bloat context
786 | {
787 | name: "analyze_repository",
788 | description: "This tool performs a comprehensive multi-layered analysis...",
789 | // 200+ tokens, excessive detail
790 | }
791 | ```
792 |
793 | **Tool Organization Metadata** (Optional Enhancement):
794 |
795 | ```typescript
796 | interface ToolMetadata {
797 | category: "analysis" | "generation" | "deployment" | "validation";
798 | complexity: "simple" | "moderate" | "complex";
799 | estimatedTokens: number;
800 | codeModeFriendly: boolean;
801 | }
802 | ```
803 |
804 | **Result Summarization for Large Outputs**:
805 |
806 | ```typescript
807 | function formatResponse(data: AnalysisResult): MCPToolResponse {
808 | if (data.size > 10_000) {
809 | return {
810 | summary: extractKeyMetrics(data),
811 | resourceUri: storeAsResource(data), // Full data via MCP resource
812 | nextSteps: [...] // Guidance without bloating context
813 | };
814 | }
815 | return { data }; // Small results returned directly
816 | }
817 | ```
818 |
819 | ### Performance Benefits in Code Mode
820 |
821 | When documcp tools are orchestrated via Code Mode clients:
822 |
823 | - **Token Efficiency**: Only needed tool definitions loaded (not all 25+)
824 | - **Parallel Execution**: Multiple tools run concurrently in sandbox
825 | - **Context Preservation**: Intermediate results stay in sandbox, not LLM context
826 | - **Cost Reduction**: ~75x savings on complex workflows
827 |
828 | For detailed analysis, see [ADR-011: CE-MCP Compatibility](adr-0011-ce-mcp-compatibility.md).
829 |
830 | ## Future Enhancements
831 |
832 | ### Planned Tool Additions
833 |
834 | - `analyzeExistingDocs`: Deep analysis of existing documentation quality and structure
835 | - `generateMigrationPlan`: Create migration plans between different documentation systems
836 | - `optimizeContent`: AI-powered content optimization and gap analysis
837 | - `validateAccessibility`: Comprehensive accessibility testing and recommendations
838 |
839 | ### API Evolution Strategy
840 |
841 | - Versioned tool schemas with backward compatibility
842 | - Deprecation notices and migration guidance
843 | - Feature flags for experimental functionality
844 | - Community feedback integration for API improvements
845 | - Code Mode optimization: Tool categorization and metadata
846 |
847 | ## References
848 |
849 | - [Model Context Protocol Specification](https://spec.modelcontextprotocol.io/)
850 | - Commit: 210a274 - feat: Add agent artifact detection and cleanup tool (#80)
851 | - GitHub Issue: #80 - Agent artifact detection and cleanup tool
852 | - [JSON Schema Validation](https://json-schema.org/)
853 | - [API Design Best Practices](https://swagger.io/resources/articles/best-practices-in-api-design/)
854 |
```
--------------------------------------------------------------------------------
/tests/utils/sitemap-generator.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for sitemap-generator utility
3 | */
4 |
5 | import { promises as fs } from "fs";
6 | import path from "path";
7 | import { tmpdir } from "os";
8 | import {
9 | generateSitemap,
10 | parseSitemap,
11 | validateSitemap,
12 | updateSitemap,
13 | listSitemapUrls,
14 | type SitemapUrl,
15 | type SitemapOptions,
16 | } from "../../src/utils/sitemap-generator.js";
17 |
18 | describe("sitemap-generator", () => {
19 | let testDir: string;
20 | let docsDir: string;
21 |
22 | beforeEach(async () => {
23 | // Create temporary test directory
24 | testDir = path.join(tmpdir(), `sitemap-test-${Date.now()}`);
25 | docsDir = path.join(testDir, "docs");
26 | await fs.mkdir(docsDir, { recursive: true });
27 | });
28 |
29 | afterEach(async () => {
30 | // Clean up test directory
31 | try {
32 | await fs.rm(testDir, { recursive: true, force: true });
33 | } catch (error) {
34 | // Ignore cleanup errors
35 | }
36 | });
37 |
38 | describe("generateSitemap", () => {
39 | it("should generate sitemap.xml from documentation files", async () => {
40 | // Create test documentation structure
41 | await fs.mkdir(path.join(docsDir, "tutorials"), { recursive: true });
42 | await fs.mkdir(path.join(docsDir, "reference"), { recursive: true });
43 |
44 | await fs.writeFile(
45 | path.join(docsDir, "index.md"),
46 | "# Home\n\nWelcome to the docs!",
47 | );
48 | await fs.writeFile(
49 | path.join(docsDir, "tutorials", "getting-started.md"),
50 | "# Getting Started\n\nStart here.",
51 | );
52 | await fs.writeFile(
53 | path.join(docsDir, "reference", "api.md"),
54 | "# API Reference\n\nAPI documentation.",
55 | );
56 |
57 | const options: SitemapOptions = {
58 | baseUrl: "https://example.com",
59 | docsPath: docsDir,
60 | useGitHistory: false,
61 | };
62 |
63 | const result = await generateSitemap(options);
64 |
65 | expect(result.xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
66 | expect(result.xml).toContain(
67 | '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
68 | );
69 | expect(result.urls).toHaveLength(3);
70 | expect(result.stats.totalUrls).toBe(3);
71 | expect(result.stats.byCategory).toHaveProperty("home");
72 | expect(result.stats.byCategory).toHaveProperty("tutorial");
73 | expect(result.stats.byCategory).toHaveProperty("reference");
74 | });
75 |
76 | it("should generate URLs with correct priorities based on categories", async () => {
77 | await fs.mkdir(path.join(docsDir, "tutorials"), { recursive: true });
78 | await fs.mkdir(path.join(docsDir, "reference"), { recursive: true });
79 |
80 | await fs.writeFile(
81 | path.join(docsDir, "tutorials", "guide.md"),
82 | "# Tutorial",
83 | );
84 | await fs.writeFile(
85 | path.join(docsDir, "reference", "api.md"),
86 | "# Reference",
87 | );
88 |
89 | const result = await generateSitemap({
90 | baseUrl: "https://example.com",
91 | docsPath: docsDir,
92 | useGitHistory: false,
93 | });
94 |
95 | const tutorialUrl = result.urls.find((u) => u.category === "tutorial");
96 | const referenceUrl = result.urls.find((u) => u.category === "reference");
97 |
98 | expect(tutorialUrl?.priority).toBe(1.0); // Highest priority
99 | expect(referenceUrl?.priority).toBe(0.8);
100 | });
101 |
102 | it("should handle empty documentation directory", async () => {
103 | const result = await generateSitemap({
104 | baseUrl: "https://example.com",
105 | docsPath: docsDir,
106 | useGitHistory: false,
107 | });
108 |
109 | expect(result.urls).toHaveLength(0);
110 | expect(result.stats.totalUrls).toBe(0);
111 | });
112 |
113 | it("should exclude node_modules and other excluded patterns", async () => {
114 | await fs.mkdir(path.join(docsDir, "node_modules"), { recursive: true });
115 | await fs.writeFile(
116 | path.join(docsDir, "node_modules", "package.md"),
117 | "# Package",
118 | );
119 | await fs.writeFile(path.join(docsDir, "guide.md"), "# Guide");
120 |
121 | const result = await generateSitemap({
122 | baseUrl: "https://example.com",
123 | docsPath: docsDir,
124 | useGitHistory: false,
125 | });
126 |
127 | expect(result.urls).toHaveLength(1);
128 | expect(result.urls[0].loc).toContain("guide.html");
129 | });
130 |
131 | it("should convert markdown extensions to html", async () => {
132 | await fs.writeFile(path.join(docsDir, "page.md"), "# Page");
133 | await fs.writeFile(path.join(docsDir, "component.mdx"), "# Component");
134 |
135 | const result = await generateSitemap({
136 | baseUrl: "https://example.com",
137 | docsPath: docsDir,
138 | useGitHistory: false,
139 | });
140 |
141 | expect(result.urls[0].loc).toContain(".html");
142 | expect(result.urls[1].loc).toContain(".html");
143 | expect(result.urls.some((u) => u.loc.endsWith(".md"))).toBe(false);
144 | expect(result.urls.some((u) => u.loc.endsWith(".mdx"))).toBe(false);
145 | });
146 |
147 | it("should extract title from markdown frontmatter", async () => {
148 | const content = `---
149 | title: My Custom Title
150 | ---
151 |
152 | # Main Heading
153 |
154 | Content here.`;
155 |
156 | await fs.writeFile(path.join(docsDir, "page.md"), content);
157 |
158 | const result = await generateSitemap({
159 | baseUrl: "https://example.com",
160 | docsPath: docsDir,
161 | useGitHistory: false,
162 | });
163 |
164 | expect(result.urls[0].title).toBe("My Custom Title");
165 | });
166 |
167 | it("should extract title from markdown heading", async () => {
168 | await fs.writeFile(
169 | path.join(docsDir, "page.md"),
170 | "# Page Title\n\nContent",
171 | );
172 |
173 | const result = await generateSitemap({
174 | baseUrl: "https://example.com",
175 | docsPath: docsDir,
176 | useGitHistory: false,
177 | });
178 |
179 | expect(result.urls[0].title).toBe("Page Title");
180 | });
181 |
182 | it("should handle custom include and exclude patterns", async () => {
183 | await fs.writeFile(path.join(docsDir, "page.md"), "# Markdown");
184 | await fs.writeFile(path.join(docsDir, "page.html"), "<h1>HTML</h1>");
185 | await fs.writeFile(path.join(docsDir, "page.txt"), "Text");
186 |
187 | const result = await generateSitemap({
188 | baseUrl: "https://example.com",
189 | docsPath: docsDir,
190 | includePatterns: ["**/*.md"],
191 | excludePatterns: [],
192 | useGitHistory: false,
193 | });
194 |
195 | expect(result.urls).toHaveLength(1);
196 | expect(result.urls[0].loc).toContain("page.html");
197 | });
198 | });
199 |
200 | describe("parseSitemap", () => {
201 | it("should parse existing sitemap.xml", async () => {
202 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
203 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
204 | <url>
205 | <loc>https://example.com/page1.html</loc>
206 | <lastmod>2025-01-01</lastmod>
207 | <changefreq>monthly</changefreq>
208 | <priority>0.8</priority>
209 | </url>
210 | <url>
211 | <loc>https://example.com/page2.html</loc>
212 | <lastmod>2025-01-02</lastmod>
213 | <changefreq>weekly</changefreq>
214 | <priority>1.0</priority>
215 | </url>
216 | </urlset>`;
217 |
218 | const sitemapPath = path.join(testDir, "sitemap.xml");
219 | await fs.writeFile(sitemapPath, xml);
220 |
221 | const urls = await parseSitemap(sitemapPath);
222 |
223 | expect(urls).toHaveLength(2);
224 | expect(urls[0].loc).toBe("https://example.com/page1.html");
225 | expect(urls[0].lastmod).toBe("2025-01-01");
226 | expect(urls[0].changefreq).toBe("monthly");
227 | expect(urls[0].priority).toBe(0.8);
228 | expect(urls[1].loc).toBe("https://example.com/page2.html");
229 | });
230 |
231 | it("should handle XML special characters", async () => {
232 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
233 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
234 | <url>
235 | <loc>https://example.com/page?id=1&type=test</loc>
236 | </url>
237 | </urlset>`;
238 |
239 | const sitemapPath = path.join(testDir, "sitemap.xml");
240 | await fs.writeFile(sitemapPath, xml);
241 |
242 | const urls = await parseSitemap(sitemapPath);
243 |
244 | expect(urls[0].loc).toBe("https://example.com/page?id=1&type=test");
245 | });
246 |
247 | it("should throw error for invalid sitemap", async () => {
248 | const sitemapPath = path.join(testDir, "invalid.xml");
249 | await fs.writeFile(sitemapPath, "not xml");
250 |
251 | const urls = await parseSitemap(sitemapPath);
252 | expect(urls).toHaveLength(0); // Graceful handling of invalid XML
253 | });
254 | });
255 |
256 | describe("validateSitemap", () => {
257 | it("should validate correct sitemap", async () => {
258 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
259 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
260 | <url>
261 | <loc>https://example.com/page.html</loc>
262 | <lastmod>2025-01-01</lastmod>
263 | <changefreq>monthly</changefreq>
264 | <priority>0.8</priority>
265 | </url>
266 | </urlset>`;
267 |
268 | const sitemapPath = path.join(testDir, "sitemap.xml");
269 | await fs.writeFile(sitemapPath, xml);
270 |
271 | const result = await validateSitemap(sitemapPath);
272 |
273 | expect(result.valid).toBe(true);
274 | expect(result.errors).toHaveLength(0);
275 | expect(result.urlCount).toBe(1);
276 | });
277 |
278 | it("should detect missing loc element", async () => {
279 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
280 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
281 | <url>
282 | <lastmod>2025-01-01</lastmod>
283 | </url>
284 | </urlset>`;
285 |
286 | const sitemapPath = path.join(testDir, "sitemap.xml");
287 | await fs.writeFile(sitemapPath, xml);
288 |
289 | const result = await validateSitemap(sitemapPath);
290 |
291 | expect(result.valid).toBe(false);
292 | expect(result.errors.some((e) => e.includes("Missing <loc>"))).toBe(true);
293 | });
294 |
295 | it("should detect invalid priority", async () => {
296 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
297 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
298 | <url>
299 | <loc>https://example.com/page.html</loc>
300 | <priority>1.5</priority>
301 | </url>
302 | </urlset>`;
303 |
304 | const sitemapPath = path.join(testDir, "sitemap.xml");
305 | await fs.writeFile(sitemapPath, xml);
306 |
307 | const result = await validateSitemap(sitemapPath);
308 |
309 | expect(result.valid).toBe(false);
310 | expect(
311 | result.errors.some((e) =>
312 | e.includes("Priority must be between 0.0 and 1.0"),
313 | ),
314 | ).toBe(true);
315 | });
316 |
317 | it("should detect invalid protocol", async () => {
318 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
319 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
320 | <url>
321 | <loc>ftp://example.com/page.html</loc>
322 | </url>
323 | </urlset>`;
324 |
325 | const sitemapPath = path.join(testDir, "sitemap.xml");
326 | await fs.writeFile(sitemapPath, xml);
327 |
328 | const result = await validateSitemap(sitemapPath);
329 |
330 | expect(result.valid).toBe(false);
331 | expect(result.errors.some((e) => e.includes("Invalid protocol"))).toBe(
332 | true,
333 | );
334 | });
335 |
336 | it("should return error if sitemap does not exist", async () => {
337 | const sitemapPath = path.join(testDir, "nonexistent.xml");
338 |
339 | const result = await validateSitemap(sitemapPath);
340 |
341 | expect(result.valid).toBe(false);
342 | expect(result.errors.some((e) => e.includes("does not exist"))).toBe(
343 | true,
344 | );
345 | });
346 | });
347 |
348 | describe("updateSitemap", () => {
349 | it("should update existing sitemap with new pages", async () => {
350 | // Create initial sitemap
351 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
352 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
353 | <url>
354 | <loc>https://example.com/page1.html</loc>
355 | <lastmod>2025-01-01</lastmod>
356 | <priority>0.8</priority>
357 | </url>
358 | </urlset>`;
359 |
360 | const sitemapPath = path.join(testDir, "sitemap.xml");
361 | await fs.writeFile(sitemapPath, xml);
362 |
363 | // Add new documentation file
364 | await fs.writeFile(path.join(docsDir, "page1.md"), "# Page 1");
365 | await fs.writeFile(path.join(docsDir, "page2.md"), "# Page 2");
366 |
367 | const changes = await updateSitemap(sitemapPath, {
368 | baseUrl: "https://example.com",
369 | docsPath: docsDir,
370 | useGitHistory: false,
371 | });
372 |
373 | expect(changes.added).toBe(1); // page2.md is new
374 | expect(changes.total).toBe(2);
375 | });
376 |
377 | it("should detect removed pages", async () => {
378 | // Create initial sitemap with 2 URLs
379 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
380 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
381 | <url>
382 | <loc>https://example.com/page1.html</loc>
383 | </url>
384 | <url>
385 | <loc>https://example.com/page2.html</loc>
386 | </url>
387 | </urlset>`;
388 |
389 | const sitemapPath = path.join(testDir, "sitemap.xml");
390 | await fs.writeFile(sitemapPath, xml);
391 |
392 | // Only create page1.md
393 | await fs.writeFile(path.join(docsDir, "page1.md"), "# Page 1");
394 |
395 | const changes = await updateSitemap(sitemapPath, {
396 | baseUrl: "https://example.com",
397 | docsPath: docsDir,
398 | useGitHistory: false,
399 | });
400 |
401 | expect(changes.removed).toBe(1); // page2.html was removed
402 | expect(changes.total).toBe(1);
403 | });
404 |
405 | it("should create new sitemap if none exists", async () => {
406 | const sitemapPath = path.join(testDir, "sitemap.xml");
407 | await fs.writeFile(path.join(docsDir, "page.md"), "# Page");
408 |
409 | const changes = await updateSitemap(sitemapPath, {
410 | baseUrl: "https://example.com",
411 | docsPath: docsDir,
412 | useGitHistory: false,
413 | });
414 |
415 | expect(changes.added).toBe(1);
416 | expect(changes.total).toBe(1);
417 |
418 | // Verify sitemap was created
419 | const exists = await fs
420 | .access(sitemapPath)
421 | .then(() => true)
422 | .catch(() => false);
423 | expect(exists).toBe(true);
424 | });
425 | });
426 |
427 | describe("listSitemapUrls", () => {
428 | it("should list all URLs from sitemap", async () => {
429 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
430 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
431 | <url>
432 | <loc>https://example.com/page1.html</loc>
433 | <priority>0.9</priority>
434 | </url>
435 | <url>
436 | <loc>https://example.com/page2.html</loc>
437 | <priority>0.8</priority>
438 | </url>
439 | </urlset>`;
440 |
441 | const sitemapPath = path.join(testDir, "sitemap.xml");
442 | await fs.writeFile(sitemapPath, xml);
443 |
444 | const urls = await listSitemapUrls(sitemapPath);
445 |
446 | expect(urls).toHaveLength(2);
447 | expect(urls[0].loc).toBe("https://example.com/page1.html");
448 | expect(urls[1].loc).toBe("https://example.com/page2.html");
449 | });
450 | });
451 |
452 | describe("edge cases", () => {
453 | it("should handle deeply nested directory structures", async () => {
454 | const deepPath = path.join(docsDir, "a", "b", "c", "d");
455 | await fs.mkdir(deepPath, { recursive: true });
456 | await fs.writeFile(path.join(deepPath, "deep.md"), "# Deep Page");
457 |
458 | const result = await generateSitemap({
459 | baseUrl: "https://example.com",
460 | docsPath: docsDir,
461 | useGitHistory: false,
462 | });
463 |
464 | expect(result.urls).toHaveLength(1);
465 | expect(result.urls[0].loc).toContain("a/b/c/d/deep.html");
466 | });
467 |
468 | it("should handle files with special characters in names", async () => {
469 | await fs.writeFile(path.join(docsDir, "my-page-2024.md"), "# Page");
470 |
471 | const result = await generateSitemap({
472 | baseUrl: "https://example.com",
473 | docsPath: docsDir,
474 | useGitHistory: false,
475 | });
476 |
477 | expect(result.urls).toHaveLength(1);
478 | expect(result.urls[0].loc).toContain("my-page-2024.html");
479 | });
480 |
481 | it("should handle index.html correctly", async () => {
482 | await fs.writeFile(path.join(docsDir, "index.md"), "# Home");
483 |
484 | const result = await generateSitemap({
485 | baseUrl: "https://example.com",
486 | docsPath: docsDir,
487 | useGitHistory: false,
488 | });
489 |
490 | expect(result.urls[0].loc).toBe("https://example.com/");
491 | });
492 |
493 | it("should exclude directories matching exclusion patterns", async () => {
494 | // Create directory structure with excluded dirs
495 | await fs.mkdir(path.join(docsDir, "node_modules"), { recursive: true });
496 | await fs.mkdir(path.join(docsDir, "valid"), { recursive: true });
497 | await fs.writeFile(
498 | path.join(docsDir, "node_modules", "package.md"),
499 | "# Should be excluded",
500 | );
501 | await fs.writeFile(
502 | path.join(docsDir, "valid", "page.md"),
503 | "# Valid Page",
504 | );
505 |
506 | const result = await generateSitemap({
507 | baseUrl: "https://example.com",
508 | docsPath: docsDir,
509 | useGitHistory: false,
510 | });
511 |
512 | // Should only include valid directory, not node_modules
513 | expect(result.urls).toHaveLength(1);
514 | expect(result.urls[0].loc).toContain("valid/page");
515 | });
516 |
517 | it("should handle directory scan errors gracefully", async () => {
518 | // Create a valid docs directory
519 | await fs.writeFile(path.join(docsDir, "valid.md"), "# Valid");
520 |
521 | const result = await generateSitemap({
522 | baseUrl: "https://example.com",
523 | docsPath: docsDir,
524 | useGitHistory: false,
525 | });
526 |
527 | // Should succeed despite potential permission issues
528 | expect(result.urls.length).toBeGreaterThanOrEqual(1);
529 | });
530 |
531 | it("should categorize explanation pages correctly", async () => {
532 | await fs.mkdir(path.join(docsDir, "explanation"), { recursive: true });
533 | await fs.writeFile(
534 | path.join(docsDir, "explanation", "concepts.md"),
535 | "# Concepts",
536 | );
537 |
538 | const result = await generateSitemap({
539 | baseUrl: "https://example.com",
540 | docsPath: docsDir,
541 | useGitHistory: false,
542 | });
543 |
544 | expect(result.stats.byCategory).toHaveProperty("explanation");
545 | expect(result.stats.byCategory.explanation).toBeGreaterThan(0);
546 | });
547 |
548 | it("should fall back to file system date when git fails", async () => {
549 | await fs.writeFile(path.join(docsDir, "no-git.md"), "# No Git");
550 |
551 | const result = await generateSitemap({
552 | baseUrl: "https://example.com",
553 | docsPath: docsDir,
554 | useGitHistory: true, // Try git but will fall back
555 | });
556 |
557 | // Should still have a lastmod date from file system
558 | expect(result.urls[0].lastmod).toBeDefined();
559 | expect(result.urls[0].lastmod).toMatch(/^\d{4}-\d{2}-\d{2}$/);
560 | });
561 |
562 | it("should handle files without extensions", async () => {
563 | await fs.writeFile(path.join(docsDir, "README"), "# Readme");
564 |
565 | const result = await generateSitemap({
566 | baseUrl: "https://example.com",
567 | docsPath: docsDir,
568 | includePatterns: ["**/*"], // Include all files
569 | useGitHistory: false,
570 | });
571 |
572 | // Should handle extensionless files
573 | expect(result.urls.length).toBeGreaterThanOrEqual(0);
574 | });
575 |
576 | it("should handle empty git timestamp", async () => {
577 | // Create file and generate sitemap with git enabled
578 | await fs.writeFile(path.join(docsDir, "test.md"), "# Test");
579 |
580 | const result = await generateSitemap({
581 | baseUrl: "https://example.com",
582 | docsPath: docsDir,
583 | useGitHistory: true,
584 | });
585 |
586 | // Should have valid dates even if git returns empty
587 | expect(result.urls[0].lastmod).toBeDefined();
588 | });
589 |
590 | it("should handle files in deeply excluded paths", async () => {
591 | await fs.mkdir(path.join(docsDir, ".git", "objects"), {
592 | recursive: true,
593 | });
594 | await fs.writeFile(
595 | path.join(docsDir, ".git", "objects", "file.md"),
596 | "# Git Object",
597 | );
598 | await fs.writeFile(path.join(docsDir, "valid.md"), "# Valid");
599 |
600 | const result = await generateSitemap({
601 | baseUrl: "https://example.com",
602 | docsPath: docsDir,
603 | useGitHistory: false,
604 | });
605 |
606 | // Should exclude .git directory
607 | expect(result.urls).toHaveLength(1);
608 | expect(result.urls[0].loc).not.toContain(".git");
609 | });
610 |
611 | it("should extract title from HTML title tag", async () => {
612 | const htmlContent = `<!DOCTYPE html>
613 | <html>
614 | <head>
615 | <title>HTML Page Title</title>
616 | </head>
617 | <body>
618 | <h1>Different Heading</h1>
619 | </body>
620 | </html>`;
621 |
622 | await fs.writeFile(path.join(docsDir, "page.html"), htmlContent);
623 |
624 | const result = await generateSitemap({
625 | baseUrl: "https://example.com",
626 | docsPath: docsDir,
627 | includePatterns: ["**/*.html"],
628 | useGitHistory: false,
629 | });
630 |
631 | expect(result.urls[0].title).toBe("HTML Page Title");
632 | });
633 |
634 | it("should handle files with no extractable title", async () => {
635 | await fs.writeFile(path.join(docsDir, "notitle.md"), "Just content");
636 |
637 | const result = await generateSitemap({
638 | baseUrl: "https://example.com",
639 | docsPath: docsDir,
640 | useGitHistory: false,
641 | });
642 |
643 | expect(result.urls[0].title).toBeUndefined();
644 | });
645 |
646 | it("should handle inaccessible files gracefully", async () => {
647 | await fs.writeFile(path.join(docsDir, "readable.md"), "# Readable");
648 |
649 | const result = await generateSitemap({
650 | baseUrl: "https://example.com",
651 | docsPath: docsDir,
652 | useGitHistory: false,
653 | });
654 |
655 | // Should still process readable files
656 | expect(result.urls.length).toBeGreaterThan(0);
657 | });
658 | });
659 |
660 | describe("validateSitemap - additional validations", () => {
661 | it("should detect empty sitemap", async () => {
662 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
663 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
664 | </urlset>`;
665 |
666 | const sitemapPath = path.join(testDir, "sitemap.xml");
667 | await fs.writeFile(sitemapPath, xml);
668 |
669 | const result = await validateSitemap(sitemapPath);
670 |
671 | expect(result.valid).toBe(true);
672 | expect(result.warnings.some((w) => w.includes("no URLs"))).toBe(true);
673 | });
674 |
675 | it("should detect URL exceeding 2048 characters", async () => {
676 | const longUrl = `https://example.com/${"a".repeat(2100)}`;
677 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
678 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
679 | <url>
680 | <loc>${longUrl}</loc>
681 | </url>
682 | </urlset>`;
683 |
684 | const sitemapPath = path.join(testDir, "sitemap.xml");
685 | await fs.writeFile(sitemapPath, xml);
686 |
687 | const result = await validateSitemap(sitemapPath);
688 |
689 | expect(result.valid).toBe(false);
690 | expect(result.errors.some((e) => e.includes("exceeds 2048"))).toBe(true);
691 | });
692 |
693 | it("should warn about invalid lastmod format", async () => {
694 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
695 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
696 | <url>
697 | <loc>https://example.com/page.html</loc>
698 | <lastmod>invalid-date</lastmod>
699 | </url>
700 | </urlset>`;
701 |
702 | const sitemapPath = path.join(testDir, "sitemap.xml");
703 | await fs.writeFile(sitemapPath, xml);
704 |
705 | const result = await validateSitemap(sitemapPath);
706 |
707 | expect(result.warnings.some((w) => w.includes("Invalid lastmod"))).toBe(
708 | true,
709 | );
710 | });
711 |
712 | it("should detect sitemap with more than 50,000 URLs", async () => {
713 | // Create sitemap XML with >50,000 URLs
714 | const urls = Array.from(
715 | { length: 50001 },
716 | (_, i) => ` <url>
717 | <loc>https://example.com/page${i}.html</loc>
718 | <lastmod>2025-01-01</lastmod>
719 | </url>`,
720 | ).join("\n");
721 |
722 | const xml = `<?xml version="1.0" encoding="UTF-8"?>
723 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
724 | ${urls}
725 | </urlset>`;
726 |
727 | const sitemapPath = path.join(testDir, "large-sitemap.xml");
728 | await fs.writeFile(sitemapPath, xml);
729 |
730 | const result = await validateSitemap(sitemapPath);
731 |
732 | expect(result.valid).toBe(false);
733 | expect(result.errors.some((e) => e.includes("50,000"))).toBe(true);
734 | });
735 |
736 | it("should handle malformed XML gracefully", async () => {
737 | // The regex-based parser is lenient and extracts data where possible
738 | // This tests that the parser doesn't crash on malformed XML
739 | const malformedXml = `<?xml version="1.0"?>
740 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
741 | <url>
742 | <loc>https://example.com</loc>
743 | </url>
744 | <!-- Missing closing urlset tag`;
745 |
746 | const sitemapPath = path.join(testDir, "malformed.xml");
747 | await fs.writeFile(sitemapPath, malformedXml);
748 |
749 | // Should parse successfully despite malformation (regex-based parsing)
750 | const result = await validateSitemap(sitemapPath);
751 | expect(result).toBeDefined();
752 | expect(result.urlCount).toBe(1);
753 | });
754 | });
755 |
756 | describe("Edge cases", () => {
757 | it("should handle excluded directories", async () => {
758 | // Create structure with node_modules
759 | await fs.mkdir(path.join(testDir, "node_modules"), { recursive: true });
760 | await fs.writeFile(
761 | path.join(testDir, "node_modules", "package.md"),
762 | "# Should be excluded",
763 | );
764 | await fs.writeFile(path.join(testDir, "included.md"), "# Included");
765 |
766 | const result = await generateSitemap({
767 | baseUrl: "https://example.com",
768 | docsPath: testDir,
769 | includePatterns: ["**/*.md"],
770 | useGitHistory: false,
771 | });
772 |
773 | expect(result.urls.some((u) => u.loc.includes("node_modules"))).toBe(
774 | false,
775 | );
776 | expect(result.urls.some((u) => u.loc.includes("included"))).toBe(true);
777 | });
778 |
779 | it("should handle directory scan errors gracefully", async () => {
780 | // Test with a path that has permission issues or doesn't exist
781 | const result = await generateSitemap({
782 | baseUrl: "https://example.com",
783 | docsPath: path.join(testDir, "nonexistent"),
784 | includePatterns: ["**/*.md"],
785 | useGitHistory: false,
786 | });
787 |
788 | expect(result.urls).toEqual([]);
789 | });
790 |
791 | it("should use git timestamp when available", async () => {
792 | // Initialize git and create a committed file
793 | await fs.writeFile(path.join(testDir, "test.md"), "# Test");
794 |
795 | try {
796 | const { execSync } = require("child_process");
797 | execSync("git init", { cwd: testDir, stdio: "ignore" });
798 | execSync("git config user.email '[email protected]'", {
799 | cwd: testDir,
800 | stdio: "ignore",
801 | });
802 | execSync("git config user.name 'Test'", {
803 | cwd: testDir,
804 | stdio: "ignore",
805 | });
806 | execSync("git add test.md", { cwd: testDir, stdio: "ignore" });
807 | execSync("git commit -m 'test'", { cwd: testDir, stdio: "ignore" });
808 |
809 | const result = await generateSitemap({
810 | baseUrl: "https://example.com",
811 | docsPath: testDir,
812 | includePatterns: ["**/*.md"],
813 | useGitHistory: true,
814 | });
815 |
816 | expect(result.urls.length).toBe(1);
817 | expect(result.urls[0].lastmod).toMatch(/\d{4}-\d{2}-\d{2}/);
818 | } catch (error) {
819 | // Git might not be available in test environment, skip
820 | console.log("Git test skipped:", error);
821 | }
822 | });
823 |
824 | it("should use current date when file doesn't exist", async () => {
825 | // This tests the getFileLastModified error path
826 | // We'll indirectly test this by ensuring dates are always returned
827 | const result = await generateSitemap({
828 | baseUrl: "https://example.com",
829 | docsPath: testDir,
830 | includePatterns: ["**/*.md"],
831 | useGitHistory: false,
832 | });
833 |
834 | // Even with no files, function should not crash
835 | expect(result).toBeDefined();
836 | expect(Array.isArray(result.urls)).toBe(true);
837 | });
838 | });
839 | });
840 |
```
--------------------------------------------------------------------------------
/src/memory/kg-health.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Knowledge Graph Health Monitoring Module
3 | * Implements Phase 2: KG Health Tracking
4 | *
5 | * Provides comprehensive health monitoring, issue detection, and trend analysis
6 | * for the DocuMCP knowledge graph to ensure data quality and performance.
7 | */
8 |
9 | import { promises as fs } from "fs";
10 | import { join } from "path";
11 | import KnowledgeGraph, { GraphNode, GraphEdge } from "./knowledge-graph.js";
12 | import { KGStorage } from "./kg-storage.js";
13 |
14 | // ============================================================================
15 | // Health Metrics Schema
16 | // ============================================================================
17 |
18 | export interface KGHealthMetrics {
19 | timestamp: string;
20 | overallHealth: number; // 0-100 score
21 | dataQuality: DataQualityMetrics;
22 | structureHealth: StructureHealthMetrics;
23 | performance: PerformanceMetrics;
24 | trends: HealthTrends;
25 | issues: HealthIssue[];
26 | recommendations: HealthRecommendation[];
27 | }
28 |
29 | export interface DataQualityMetrics {
30 | score: number; // 0-100
31 | staleNodeCount: number; // nodes not updated in 30+ days
32 | orphanedEdgeCount: number;
33 | duplicateCount: number;
34 | confidenceAverage: number;
35 | completenessScore: number; // % of expected relationships present
36 | totalNodes: number;
37 | totalEdges: number;
38 | }
39 |
40 | export interface StructureHealthMetrics {
41 | score: number; // 0-100
42 | isolatedNodeCount: number; // nodes with no edges
43 | clusteringCoefficient: number;
44 | averagePathLength: number;
45 | densityScore: number;
46 | connectedComponents: number;
47 | }
48 |
49 | export interface PerformanceMetrics {
50 | score: number; // 0-100
51 | avgQueryTime: number; // ms
52 | storageSize: number; // bytes
53 | growthRate: number; // bytes/day
54 | indexEfficiency: number;
55 | }
56 |
57 | export interface HealthTrends {
58 | healthTrend: "improving" | "stable" | "degrading";
59 | nodeGrowthRate: number; // nodes/day
60 | edgeGrowthRate: number; // edges/day
61 | errorRate: number; // errors/operations (from last 100 operations)
62 | qualityTrend: "improving" | "stable" | "degrading";
63 | }
64 |
65 | export interface HealthIssue {
66 | id: string;
67 | severity: "critical" | "high" | "medium" | "low";
68 | category: "integrity" | "performance" | "quality" | "structure";
69 | description: string;
70 | affectedEntities: string[];
71 | remediation: string;
72 | detectedAt: string;
73 | autoFixable: boolean;
74 | }
75 |
76 | export interface HealthRecommendation {
77 | id: string;
78 | priority: "high" | "medium" | "low";
79 | action: string;
80 | expectedImpact: number; // health score increase (0-100)
81 | effort: "low" | "medium" | "high";
82 | category: string;
83 | }
84 |
85 | export interface HealthHistory {
86 | timestamp: string;
87 | overallHealth: number;
88 | dataQuality: number;
89 | structureHealth: number;
90 | performance: number;
91 | nodeCount: number;
92 | edgeCount: number;
93 | }
94 |
95 | // ============================================================================
96 | // Health Monitoring Class
97 | // ============================================================================
98 |
99 | export class KGHealthMonitor {
100 | private storageDir: string;
101 | private historyFilePath: string;
102 | private issueDetectors: IssueDetector[];
103 | private performanceTracking: PerformanceTracker;
104 |
105 | constructor(storageDir?: string) {
106 | this.storageDir = storageDir || `${process.cwd()}/.documcp/memory`;
107 | this.historyFilePath = join(this.storageDir, "health-history.jsonl");
108 | this.issueDetectors = createIssueDetectors();
109 | this.performanceTracking = new PerformanceTracker();
110 | }
111 |
112 | /**
113 | * Calculate comprehensive health metrics
114 | */
115 | async calculateHealth(
116 | kg: KnowledgeGraph,
117 | storage: KGStorage,
118 | ): Promise<KGHealthMetrics> {
119 | const timestamp = new Date().toISOString();
120 |
121 | // Calculate component metrics
122 | const dataQuality = await this.calculateDataQuality(kg, storage);
123 | const structureHealth = await this.calculateStructureHealth(kg);
124 | const performance = await this.calculatePerformance(storage);
125 |
126 | // Calculate overall health (weighted average)
127 | const overallHealth = Math.round(
128 | dataQuality.score * 0.4 +
129 | structureHealth.score * 0.3 +
130 | performance.score * 0.3,
131 | );
132 |
133 | // Detect issues
134 | const issues = await this.detectIssues(kg, {
135 | dataQuality,
136 | structureHealth,
137 | performance,
138 | });
139 |
140 | // Generate recommendations
141 | const recommendations = this.generateRecommendations(issues, {
142 | dataQuality,
143 | structureHealth,
144 | performance,
145 | });
146 |
147 | // Analyze trends
148 | const trends = await this.analyzeTrends(overallHealth);
149 |
150 | const metrics: KGHealthMetrics = {
151 | timestamp,
152 | overallHealth,
153 | dataQuality,
154 | structureHealth,
155 | performance,
156 | trends,
157 | issues,
158 | recommendations,
159 | };
160 |
161 | // Track history
162 | await this.trackHealthHistory(metrics);
163 |
164 | return metrics;
165 | }
166 |
167 | /**
168 | * Calculate data quality metrics
169 | */
170 | private async calculateDataQuality(
171 | kg: KnowledgeGraph,
172 | storage: KGStorage,
173 | ): Promise<DataQualityMetrics> {
174 | await kg.getStatistics();
175 | const integrity = await storage.verifyIntegrity();
176 |
177 | const now = new Date();
178 | const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
179 |
180 | // Count stale nodes
181 | const allNodes = await kg.getAllNodes();
182 | const staleNodeCount = allNodes.filter((node) => {
183 | const lastUpdated = new Date(node.lastUpdated);
184 | return lastUpdated < thirtyDaysAgo;
185 | }).length;
186 |
187 | // Get orphaned edges from integrity check
188 | const orphanedEdgeCount = integrity.warnings.filter((w) =>
189 | w.includes("missing"),
190 | ).length;
191 |
192 | // Get duplicate count from integrity check
193 | const duplicateCount = integrity.errors.filter((e) =>
194 | e.includes("Duplicate"),
195 | ).length;
196 |
197 | // Calculate average confidence
198 | const allEdges = await kg.getAllEdges();
199 | const confidenceAverage =
200 | allEdges.length > 0
201 | ? allEdges.reduce((sum, edge) => sum + edge.confidence, 0) /
202 | allEdges.length
203 | : 1.0;
204 |
205 | // Calculate completeness (% of projects with expected relationships)
206 | const completenessScore = this.calculateCompleteness(allNodes, allEdges);
207 |
208 | // Calculate data quality score (0-100)
209 | const stalePercentage =
210 | (staleNodeCount / Math.max(allNodes.length, 1)) * 100;
211 | const orphanPercentage =
212 | (orphanedEdgeCount / Math.max(allEdges.length, 1)) * 100;
213 | const qualityDeductions =
214 | stalePercentage * 0.3 + orphanPercentage * 0.5 + duplicateCount * 10;
215 |
216 | const score = Math.max(
217 | 0,
218 | Math.min(100, 100 - qualityDeductions + (completenessScore - 0.5) * 50),
219 | );
220 |
221 | return {
222 | score: Math.round(score),
223 | staleNodeCount,
224 | orphanedEdgeCount,
225 | duplicateCount,
226 | confidenceAverage,
227 | completenessScore,
228 | totalNodes: allNodes.length,
229 | totalEdges: allEdges.length,
230 | };
231 | }
232 |
233 | /**
234 | * Calculate structure health metrics
235 | */
236 | private async calculateStructureHealth(
237 | kg: KnowledgeGraph,
238 | ): Promise<StructureHealthMetrics> {
239 | await kg.getStatistics();
240 | const allNodes = await kg.getAllNodes();
241 | const allEdges = await kg.getAllEdges();
242 |
243 | // Count isolated nodes (no edges)
244 | const nodeConnections = new Map<string, number>();
245 | for (const edge of allEdges) {
246 | nodeConnections.set(
247 | edge.source,
248 | (nodeConnections.get(edge.source) || 0) + 1,
249 | );
250 | nodeConnections.set(
251 | edge.target,
252 | (nodeConnections.get(edge.target) || 0) + 1,
253 | );
254 | }
255 |
256 | const isolatedNodeCount = allNodes.filter(
257 | (node) => !nodeConnections.has(node.id),
258 | ).length;
259 |
260 | // Calculate clustering coefficient (simplified)
261 | const clusteringCoefficient = this.calculateClusteringCoefficient(
262 | allNodes,
263 | allEdges,
264 | );
265 |
266 | // Calculate average path length (simplified - using BFS on sample)
267 | const averagePathLength = this.calculateAveragePathLength(
268 | allNodes,
269 | allEdges,
270 | );
271 |
272 | // Calculate density score
273 | const maxPossibleEdges = (allNodes.length * (allNodes.length - 1)) / 2;
274 | const densityScore =
275 | maxPossibleEdges > 0 ? allEdges.length / maxPossibleEdges : 0;
276 |
277 | // Count connected components
278 | const connectedComponents = this.countConnectedComponents(
279 | allNodes,
280 | allEdges,
281 | );
282 |
283 | // Calculate structure health score
284 | const isolatedPercentage =
285 | (isolatedNodeCount / Math.max(allNodes.length, 1)) * 100;
286 | const score = Math.max(
287 | 0,
288 | Math.min(
289 | 100,
290 | 100 -
291 | isolatedPercentage * 0.5 +
292 | clusteringCoefficient * 20 -
293 | (connectedComponents - 1) * 5,
294 | ),
295 | );
296 |
297 | return {
298 | score: Math.round(score),
299 | isolatedNodeCount,
300 | clusteringCoefficient,
301 | averagePathLength,
302 | densityScore,
303 | connectedComponents,
304 | };
305 | }
306 |
307 | /**
308 | * Calculate performance metrics
309 | */
310 | private async calculatePerformance(
311 | storage: KGStorage,
312 | ): Promise<PerformanceMetrics> {
313 | const storageStats = await storage.getStatistics();
314 |
315 | // Get average query time from performance tracker
316 | const avgQueryTime = this.performanceTracking.getAverageQueryTime();
317 |
318 | // Calculate storage size
319 | const storageSize =
320 | storageStats.fileSize.entities + storageStats.fileSize.relationships;
321 |
322 | // Calculate growth rate (bytes/day) from history
323 | const growthRate = await this.calculateGrowthRate();
324 |
325 | // Index efficiency (placeholder - would need actual indexing metrics)
326 | const indexEfficiency = 0.8;
327 |
328 | // Calculate performance score
329 | const queryScore =
330 | avgQueryTime < 10 ? 100 : Math.max(0, 100 - avgQueryTime);
331 | const sizeScore =
332 | storageSize < 10 * 1024 * 1024
333 | ? 100
334 | : Math.max(0, 100 - storageSize / (1024 * 1024));
335 | const score = Math.round(
336 | queryScore * 0.5 + sizeScore * 0.3 + indexEfficiency * 100 * 0.2,
337 | );
338 |
339 | return {
340 | score,
341 | avgQueryTime,
342 | storageSize,
343 | growthRate,
344 | indexEfficiency,
345 | };
346 | }
347 |
348 | /**
349 | * Detect issues in the knowledge graph
350 | */
351 | private async detectIssues(
352 | kg: KnowledgeGraph,
353 | metrics: {
354 | dataQuality: DataQualityMetrics;
355 | structureHealth: StructureHealthMetrics;
356 | performance: PerformanceMetrics;
357 | },
358 | ): Promise<HealthIssue[]> {
359 | const issues: HealthIssue[] = [];
360 |
361 | for (const detector of this.issueDetectors) {
362 | const detectedIssues = await detector.detect(kg, metrics);
363 | issues.push(...detectedIssues);
364 | }
365 |
366 | // Sort by severity
367 | issues.sort((a, b) => {
368 | const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
369 | return severityOrder[a.severity] - severityOrder[b.severity];
370 | });
371 |
372 | return issues;
373 | }
374 |
375 | /**
376 | * Generate recommendations based on issues and metrics
377 | */
378 | private generateRecommendations(
379 | issues: HealthIssue[],
380 | metrics: {
381 | dataQuality: DataQualityMetrics;
382 | structureHealth: StructureHealthMetrics;
383 | performance: PerformanceMetrics;
384 | },
385 | ): HealthRecommendation[] {
386 | const recommendations: HealthRecommendation[] = [];
387 |
388 | // Generate recommendations for critical/high severity issues
389 | for (const issue of issues.filter(
390 | (i) => i.severity === "critical" || i.severity === "high",
391 | )) {
392 | if (issue.autoFixable) {
393 | recommendations.push({
394 | id: `fix_${issue.id}`,
395 | priority: "high",
396 | action: issue.remediation,
397 | expectedImpact: issue.severity === "critical" ? 20 : 10,
398 | effort: "low",
399 | category: issue.category,
400 | });
401 | }
402 | }
403 |
404 | // Data quality recommendations
405 | if (metrics.dataQuality.score < 70) {
406 | if (metrics.dataQuality.staleNodeCount > 10) {
407 | recommendations.push({
408 | id: "refresh_stale_data",
409 | priority: "medium",
410 | action: `Re-analyze ${metrics.dataQuality.staleNodeCount} stale projects to refresh data`,
411 | expectedImpact: 15,
412 | effort: "medium",
413 | category: "data_quality",
414 | });
415 | }
416 |
417 | if (metrics.dataQuality.orphanedEdgeCount > 5) {
418 | recommendations.push({
419 | id: "cleanup_orphaned_edges",
420 | priority: "high",
421 | action: "Run automated cleanup to remove orphaned relationships",
422 | expectedImpact: 10,
423 | effort: "low",
424 | category: "data_quality",
425 | });
426 | }
427 | }
428 |
429 | // Structure health recommendations
430 | if (metrics.structureHealth.score < 70) {
431 | if (metrics.structureHealth.isolatedNodeCount > 0) {
432 | recommendations.push({
433 | id: "connect_isolated_nodes",
434 | priority: "medium",
435 | action: `Review and connect ${metrics.structureHealth.isolatedNodeCount} isolated nodes`,
436 | expectedImpact: 8,
437 | effort: "medium",
438 | category: "structure",
439 | });
440 | }
441 | }
442 |
443 | // Performance recommendations
444 | if (metrics.performance.score < 70) {
445 | if (metrics.performance.storageSize > 50 * 1024 * 1024) {
446 | recommendations.push({
447 | id: "optimize_storage",
448 | priority: "medium",
449 | action: "Archive or compress old knowledge graph data",
450 | expectedImpact: 12,
451 | effort: "high",
452 | category: "performance",
453 | });
454 | }
455 | }
456 |
457 | // Sort by priority and expected impact
458 | recommendations.sort((a, b) => {
459 | const priorityOrder = { high: 0, medium: 1, low: 2 };
460 | if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
461 | return priorityOrder[a.priority] - priorityOrder[b.priority];
462 | }
463 | return b.expectedImpact - a.expectedImpact;
464 | });
465 |
466 | return recommendations.slice(0, 5); // Top 5 recommendations
467 | }
468 |
469 | /**
470 | * Analyze trends from historical health data
471 | */
472 | private async analyzeTrends(currentHealth: number): Promise<HealthTrends> {
473 | const history = await this.getHealthHistory(7); // Last 7 days
474 |
475 | if (history.length < 2) {
476 | return {
477 | healthTrend: "stable",
478 | nodeGrowthRate: 0,
479 | edgeGrowthRate: 0,
480 | errorRate: 0,
481 | qualityTrend: "stable",
482 | };
483 | }
484 |
485 | // Calculate health trend
486 | const sevenDayAvg =
487 | history.reduce((sum, h) => sum + h.overallHealth, 0) / history.length;
488 | const healthDiff = currentHealth - sevenDayAvg;
489 |
490 | const healthTrend =
491 | healthDiff > 5 ? "improving" : healthDiff < -5 ? "degrading" : "stable";
492 |
493 | // Calculate growth rates
494 | const oldestEntry = history[history.length - 1];
495 | const newestEntry = history[0];
496 | const daysDiff = Math.max(
497 | 1,
498 | (new Date(newestEntry.timestamp).getTime() -
499 | new Date(oldestEntry.timestamp).getTime()) /
500 | (1000 * 60 * 60 * 24),
501 | );
502 |
503 | const nodeGrowthRate =
504 | (newestEntry.nodeCount - oldestEntry.nodeCount) / daysDiff;
505 | const edgeGrowthRate =
506 | (newestEntry.edgeCount - oldestEntry.edgeCount) / daysDiff;
507 |
508 | // Quality trend
509 | const qualityAvg =
510 | history.reduce((sum, h) => sum + h.dataQuality, 0) / history.length;
511 | const qualityDiff = history[0].dataQuality - qualityAvg;
512 |
513 | const qualityTrend =
514 | qualityDiff > 5 ? "improving" : qualityDiff < -5 ? "degrading" : "stable";
515 |
516 | return {
517 | healthTrend,
518 | nodeGrowthRate: Math.round(nodeGrowthRate * 10) / 10,
519 | edgeGrowthRate: Math.round(edgeGrowthRate * 10) / 10,
520 | errorRate: 0, // TODO: Track from operations log
521 | qualityTrend,
522 | };
523 | }
524 |
525 | /**
526 | * Track health history to persistent storage
527 | */
528 | private async trackHealthHistory(metrics: KGHealthMetrics): Promise<void> {
529 | const historyEntry: HealthHistory = {
530 | timestamp: metrics.timestamp,
531 | overallHealth: metrics.overallHealth,
532 | dataQuality: metrics.dataQuality.score,
533 | structureHealth: metrics.structureHealth.score,
534 | performance: metrics.performance.score,
535 | nodeCount: metrics.dataQuality.totalNodes,
536 | edgeCount: metrics.dataQuality.totalEdges,
537 | };
538 |
539 | try {
540 | await fs.appendFile(
541 | this.historyFilePath,
542 | JSON.stringify(historyEntry) + "\n",
543 | "utf-8",
544 | );
545 |
546 | // Keep only last 90 days of history
547 | await this.pruneHistoryFile(90);
548 | } catch (error) {
549 | console.warn("Failed to track health history:", error);
550 | }
551 | }
552 |
553 | /**
554 | * Get health history for the last N days
555 | */
556 | private async getHealthHistory(days: number): Promise<HealthHistory[]> {
557 | try {
558 | const content = await fs.readFile(this.historyFilePath, "utf-8");
559 | const lines = content.trim().split("\n");
560 |
561 | const cutoffDate = new Date();
562 | cutoffDate.setDate(cutoffDate.getDate() - days);
563 |
564 | const history: HealthHistory[] = [];
565 | for (const line of lines) {
566 | if (line.trim()) {
567 | const entry = JSON.parse(line) as HealthHistory;
568 | if (new Date(entry.timestamp) >= cutoffDate) {
569 | history.push(entry);
570 | }
571 | }
572 | }
573 |
574 | return history.reverse(); // Most recent first
575 | } catch {
576 | return [];
577 | }
578 | }
579 |
580 | /**
581 | * Prune history file to keep only last N days
582 | */
583 | private async pruneHistoryFile(days: number): Promise<void> {
584 | try {
585 | const history = await this.getHealthHistory(days);
586 | const content = history.map((h) => JSON.stringify(h)).join("\n") + "\n";
587 | await fs.writeFile(this.historyFilePath, content, "utf-8");
588 | } catch (error) {
589 | console.warn("Failed to prune history file:", error);
590 | }
591 | }
592 |
593 | // Helper methods
594 |
595 | private calculateCompleteness(
596 | nodes: GraphNode[],
597 | edges: GraphEdge[],
598 | ): number {
599 | const projectNodes = nodes.filter((n) => n.type === "project");
600 | if (projectNodes.length === 0) return 1.0;
601 |
602 | let totalExpected = 0;
603 | let totalFound = 0;
604 |
605 | for (const project of projectNodes) {
606 | // Expected relationships for each project:
607 | // 1. At least one technology relationship
608 | // 2. Documentation relationship (if hasDocs = true)
609 | // 3. Configuration relationship (if deployed)
610 |
611 | totalExpected += 1; // Technology
612 |
613 | const projectEdges = edges.filter((e) => e.source === project.id);
614 |
615 | if (projectEdges.some((e) => e.type === "project_uses_technology")) {
616 | totalFound += 1;
617 | }
618 |
619 | if (project.properties.hasDocs) {
620 | totalExpected += 1;
621 | if (
622 | projectEdges.some(
623 | (e) =>
624 | e.type === "depends_on" &&
625 | nodes.find((n) => n.id === e.target)?.type ===
626 | "documentation_section",
627 | )
628 | ) {
629 | totalFound += 1;
630 | }
631 | }
632 | }
633 |
634 | return totalExpected > 0 ? totalFound / totalExpected : 1.0;
635 | }
636 |
637 | private calculateClusteringCoefficient(
638 | nodes: GraphNode[],
639 | edges: GraphEdge[],
640 | ): number {
641 | // Simplified clustering coefficient calculation
642 | if (nodes.length < 3) return 0;
643 |
644 | const adjacency = new Map<string, Set<string>>();
645 | for (const edge of edges) {
646 | if (!adjacency.has(edge.source)) {
647 | adjacency.set(edge.source, new Set());
648 | }
649 | adjacency.get(edge.source)!.add(edge.target);
650 | }
651 |
652 | let totalCoefficient = 0;
653 | let nodeCount = 0;
654 |
655 | for (const node of nodes.slice(0, 100)) {
656 | // Sample first 100 nodes
657 | const neighbors = adjacency.get(node.id);
658 | if (!neighbors || neighbors.size < 2) continue;
659 |
660 | const neighborArray = Array.from(neighbors);
661 | let triangles = 0;
662 | const possibleTriangles =
663 | (neighborArray.length * (neighborArray.length - 1)) / 2;
664 |
665 | for (let i = 0; i < neighborArray.length; i++) {
666 | for (let j = i + 1; j < neighborArray.length; j++) {
667 | const n1Neighbors = adjacency.get(neighborArray[i]);
668 | if (n1Neighbors?.has(neighborArray[j])) {
669 | triangles++;
670 | }
671 | }
672 | }
673 |
674 | if (possibleTriangles > 0) {
675 | totalCoefficient += triangles / possibleTriangles;
676 | nodeCount++;
677 | }
678 | }
679 |
680 | return nodeCount > 0 ? totalCoefficient / nodeCount : 0;
681 | }
682 |
683 | private calculateAveragePathLength(
684 | nodes: GraphNode[],
685 | edges: GraphEdge[],
686 | ): number {
687 | // Simplified using sample BFS
688 | if (nodes.length === 0) return 0;
689 |
690 | const adjacency = new Map<string, string[]>();
691 | for (const edge of edges) {
692 | if (!adjacency.has(edge.source)) {
693 | adjacency.set(edge.source, []);
694 | }
695 | adjacency.get(edge.source)!.push(edge.target);
696 | }
697 |
698 | // Sample 10 random nodes for BFS
699 | const sampleSize = Math.min(10, nodes.length);
700 | let totalPathLength = 0;
701 | let pathCount = 0;
702 |
703 | for (let i = 0; i < sampleSize; i++) {
704 | const startNode = nodes[i];
705 | const distances = new Map<string, number>();
706 | const queue = [startNode.id];
707 | distances.set(startNode.id, 0);
708 |
709 | while (queue.length > 0) {
710 | const current = queue.shift()!;
711 | const currentDist = distances.get(current)!;
712 |
713 | const neighbors = adjacency.get(current) || [];
714 | for (const neighbor of neighbors) {
715 | if (!distances.has(neighbor)) {
716 | distances.set(neighbor, currentDist + 1);
717 | queue.push(neighbor);
718 | }
719 | }
720 | }
721 |
722 | for (const dist of distances.values()) {
723 | if (dist > 0) {
724 | totalPathLength += dist;
725 | pathCount++;
726 | }
727 | }
728 | }
729 |
730 | return pathCount > 0 ? totalPathLength / pathCount : 0;
731 | }
732 |
733 | private countConnectedComponents(
734 | nodes: GraphNode[],
735 | edges: GraphEdge[],
736 | ): number {
737 | if (nodes.length === 0) return 0;
738 |
739 | const adjacency = new Map<string, Set<string>>();
740 | for (const edge of edges) {
741 | if (!adjacency.has(edge.source)) {
742 | adjacency.set(edge.source, new Set());
743 | }
744 | if (!adjacency.has(edge.target)) {
745 | adjacency.set(edge.target, new Set());
746 | }
747 | adjacency.get(edge.source)!.add(edge.target);
748 | adjacency.get(edge.target)!.add(edge.source);
749 | }
750 |
751 | const visited = new Set<string>();
752 | let components = 0;
753 |
754 | for (const node of nodes) {
755 | if (!visited.has(node.id)) {
756 | components++;
757 | const queue = [node.id];
758 |
759 | while (queue.length > 0) {
760 | const current = queue.shift()!;
761 | if (visited.has(current)) continue;
762 |
763 | visited.add(current);
764 | const neighbors = adjacency.get(current) || new Set();
765 | for (const neighbor of neighbors) {
766 | if (!visited.has(neighbor)) {
767 | queue.push(neighbor);
768 | }
769 | }
770 | }
771 | }
772 | }
773 |
774 | return components;
775 | }
776 |
777 | private async calculateGrowthRate(): Promise<number> {
778 | const history = await this.getHealthHistory(30);
779 | if (history.length < 2) return 0;
780 |
781 | // Calculate storage size growth (simplified)
782 | return 1024; // Placeholder: 1KB/day
783 | }
784 | }
785 |
786 | // ============================================================================
787 | // Issue Detectors
788 | // ============================================================================
789 |
790 | interface IssueDetector {
791 | name: string;
792 | detect(
793 | kg: KnowledgeGraph,
794 | metrics: {
795 | dataQuality: DataQualityMetrics;
796 | structureHealth: StructureHealthMetrics;
797 | performance: PerformanceMetrics;
798 | },
799 | ): Promise<HealthIssue[]>;
800 | }
801 |
802 | function createIssueDetectors(): IssueDetector[] {
803 | return [
804 | {
805 | name: "orphaned_edges",
806 | async detect(kg, metrics) {
807 | if (metrics.dataQuality.orphanedEdgeCount > 10) {
808 | return [
809 | {
810 | id: "orphaned_edges_high",
811 | severity: "high",
812 | category: "integrity",
813 | description: `Found ${metrics.dataQuality.orphanedEdgeCount} orphaned relationships`,
814 | affectedEntities: [],
815 | remediation: "Run kg.removeOrphanedEdges() to clean up",
816 | detectedAt: new Date().toISOString(),
817 | autoFixable: true,
818 | },
819 | ];
820 | }
821 | return [];
822 | },
823 | },
824 | {
825 | name: "stale_data",
826 | async detect(kg, metrics) {
827 | if (metrics.dataQuality.staleNodeCount > 20) {
828 | return [
829 | {
830 | id: "stale_data_high",
831 | severity: "medium",
832 | category: "quality",
833 | description: `${metrics.dataQuality.staleNodeCount} nodes haven't been updated in 30+ days`,
834 | affectedEntities: [],
835 | remediation: "Re-analyze stale projects to refresh data",
836 | detectedAt: new Date().toISOString(),
837 | autoFixable: false,
838 | },
839 | ];
840 | }
841 | return [];
842 | },
843 | },
844 | {
845 | name: "low_completeness",
846 | async detect(kg, metrics) {
847 | if (metrics.dataQuality.completenessScore < 0.7) {
848 | return [
849 | {
850 | id: "low_completeness",
851 | severity: "high",
852 | category: "quality",
853 | description: `Completeness score is ${Math.round(
854 | metrics.dataQuality.completenessScore * 100,
855 | )}%`,
856 | affectedEntities: [],
857 | remediation: "Review projects for missing relationships",
858 | detectedAt: new Date().toISOString(),
859 | autoFixable: false,
860 | },
861 | ];
862 | }
863 | return [];
864 | },
865 | },
866 | {
867 | name: "isolated_nodes",
868 | async detect(kg, metrics) {
869 | const threshold = metrics.structureHealth.isolatedNodeCount;
870 | if (threshold > metrics.dataQuality.totalNodes * 0.05) {
871 | return [
872 | {
873 | id: "isolated_nodes_high",
874 | severity: "medium",
875 | category: "structure",
876 | description: `${threshold} nodes are isolated (no connections)`,
877 | affectedEntities: [],
878 | remediation: "Review and connect isolated nodes",
879 | detectedAt: new Date().toISOString(),
880 | autoFixable: false,
881 | },
882 | ];
883 | }
884 | return [];
885 | },
886 | },
887 | {
888 | name: "duplicate_entities",
889 | async detect(kg, metrics) {
890 | if (metrics.dataQuality.duplicateCount > 0) {
891 | return [
892 | {
893 | id: "duplicate_entities",
894 | severity: "critical",
895 | category: "integrity",
896 | description: `Found ${metrics.dataQuality.duplicateCount} duplicate entities`,
897 | affectedEntities: [],
898 | remediation: "Merge duplicate entities",
899 | detectedAt: new Date().toISOString(),
900 | autoFixable: false,
901 | },
902 | ];
903 | }
904 | return [];
905 | },
906 | },
907 | ];
908 | }
909 |
910 | // ============================================================================
911 | // Performance Tracker
912 | // ============================================================================
913 |
914 | class PerformanceTracker {
915 | private queryTimes: number[] = [];
916 | private maxSamples = 100;
917 |
918 | trackQuery(timeMs: number): void {
919 | this.queryTimes.push(timeMs);
920 | if (this.queryTimes.length > this.maxSamples) {
921 | this.queryTimes.shift();
922 | }
923 | }
924 |
925 | getAverageQueryTime(): number {
926 | if (this.queryTimes.length === 0) return 0;
927 | return (
928 | this.queryTimes.reduce((sum, t) => sum + t, 0) / this.queryTimes.length
929 | );
930 | }
931 | }
932 |
```
--------------------------------------------------------------------------------
/src/tools/readme-best-practices.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { readFile, writeFile, mkdir } from "fs/promises";
2 | import { join } from "path";
3 | import { z } from "zod";
4 | import { MCPToolResponse } from "../types/api.js";
5 |
6 | // Input validation schema
7 | const ReadmeBestPracticesInputSchema = z.object({
8 | readme_path: z.string().describe("Path to the README file to analyze"),
9 | project_type: z
10 | .enum(["library", "application", "tool", "documentation", "framework"])
11 | .optional()
12 | .default("library")
13 | .describe("Type of project for tailored analysis"),
14 | generate_template: z
15 | .boolean()
16 | .optional()
17 | .default(false)
18 | .describe("Generate README templates and community files"),
19 | output_directory: z
20 | .string()
21 | .optional()
22 | .describe("Directory to write generated templates and community files"),
23 | include_community_files: z
24 | .boolean()
25 | .optional()
26 | .default(true)
27 | .describe(
28 | "Generate community health files (CONTRIBUTING.md, CODE_OF_CONDUCT.md, etc.)",
29 | ),
30 | target_audience: z
31 | .enum(["beginner", "intermediate", "advanced", "mixed"])
32 | .optional()
33 | .default("mixed")
34 | .describe("Target audience for recommendations"),
35 | });
36 |
37 | type ReadmeBestPracticesInput = z.infer<typeof ReadmeBestPracticesInputSchema>;
38 |
39 | interface ChecklistItem {
40 | category: string;
41 | item: string;
42 | present: boolean;
43 | severity: "critical" | "important" | "recommended";
44 | description: string;
45 | example?: string;
46 | }
47 |
48 | interface BestPracticesReport {
49 | overallScore: number;
50 | grade: string;
51 | checklist: ChecklistItem[];
52 | recommendations: string[];
53 | templates: Record<string, string>;
54 | communityFiles: Record<string, string>;
55 | summary: {
56 | criticalIssues: number;
57 | importantIssues: number;
58 | recommendedImprovements: number;
59 | sectionsPresent: number;
60 | totalSections: number;
61 | estimatedImprovementTime: string;
62 | };
63 | }
64 |
65 | export async function readmeBestPractices(
66 | input: Partial<ReadmeBestPracticesInput>,
67 | ): Promise<
68 | MCPToolResponse<{
69 | bestPracticesReport: BestPracticesReport;
70 | recommendations: string[];
71 | nextSteps: string[];
72 | }>
73 | > {
74 | const startTime = Date.now();
75 |
76 | try {
77 | // Validate input with defaults
78 | const validatedInput = ReadmeBestPracticesInputSchema.parse(input);
79 | const {
80 | readme_path,
81 | project_type,
82 | generate_template,
83 | output_directory,
84 | include_community_files,
85 | target_audience,
86 | } = validatedInput;
87 |
88 | // Read README content
89 | let readmeContent = "";
90 | try {
91 | readmeContent = await readFile(readme_path, "utf-8");
92 | } catch (error) {
93 | if (!generate_template) {
94 | return {
95 | success: false,
96 | error: {
97 | code: "README_NOT_FOUND",
98 | message:
99 | "README file not found. Use generate_template: true to create a new README.",
100 | details: error instanceof Error ? error.message : "Unknown error",
101 | resolution:
102 | "Set generate_template: true to create a new README from template",
103 | },
104 | metadata: {
105 | toolVersion: "1.0.0",
106 | executionTime: Date.now() - startTime,
107 | timestamp: new Date().toISOString(),
108 | },
109 | };
110 | }
111 | }
112 |
113 | // Generate checklist based on project type and content
114 | const checklist = generateChecklist(
115 | readmeContent,
116 | project_type,
117 | target_audience,
118 | );
119 |
120 | // Calculate overall score
121 | const { score, grade } = calculateOverallScore(checklist);
122 |
123 | // Generate recommendations
124 | const recommendations = generateRecommendations(
125 | checklist,
126 | project_type,
127 | target_audience,
128 | );
129 |
130 | // Generate templates if requested
131 | const templates = generate_template
132 | ? generateTemplates(project_type, generate_template)
133 | : {};
134 |
135 | // Generate community files if requested
136 | const communityFiles = include_community_files
137 | ? generateCommunityFiles(project_type)
138 | : {};
139 |
140 | // Calculate summary metrics
141 | const summary = calculateSummaryMetrics(checklist);
142 |
143 | // Write files if output directory specified
144 | if (output_directory && generate_template) {
145 | await writeGeneratedFiles(
146 | templates,
147 | communityFiles,
148 | output_directory,
149 | readme_path,
150 | );
151 | }
152 |
153 | const report: BestPracticesReport = {
154 | overallScore: score,
155 | grade,
156 | checklist,
157 | recommendations,
158 | templates,
159 | communityFiles,
160 | summary,
161 | };
162 |
163 | const nextSteps = generateNextSteps(
164 | report.checklist,
165 | true,
166 | output_directory,
167 | );
168 |
169 | return {
170 | success: true,
171 | data: {
172 | bestPracticesReport: report,
173 | recommendations,
174 | nextSteps,
175 | },
176 | metadata: {
177 | toolVersion: "1.0.0",
178 | executionTime: Date.now() - startTime,
179 | timestamp: new Date().toISOString(),
180 | analysisId: `readme-best-practices-${Date.now()}`,
181 | },
182 | };
183 | } catch (error) {
184 | return {
185 | success: false,
186 | error: {
187 | code: "ANALYSIS_FAILED",
188 | message: "Failed to analyze README best practices",
189 | details: error instanceof Error ? error.message : "Unknown error",
190 | resolution:
191 | "Check README file path and permissions, ensure valid project type",
192 | },
193 | metadata: {
194 | toolVersion: "1.0.0",
195 | executionTime: Date.now() - startTime,
196 | timestamp: new Date().toISOString(),
197 | },
198 | };
199 | }
200 | }
201 |
202 | function generateChecklist(
203 | content: string,
204 | projectType: string,
205 | _targetAudience: string,
206 | ): ChecklistItem[] {
207 | const checklist: ChecklistItem[] = [];
208 | const lines = content.split("\n");
209 | const lowerContent = content.toLowerCase();
210 |
211 | // Essential Sections
212 | checklist.push({
213 | category: "Essential Sections",
214 | item: "Project Title",
215 | present: /^#\s+.+/m.test(content),
216 | severity: "critical",
217 | description: "Clear, descriptive project title as main heading",
218 | example: "# My Awesome Project",
219 | });
220 |
221 | checklist.push({
222 | category: "Essential Sections",
223 | item: "One-line Description",
224 | present:
225 | />\s*.+/.test(content) ||
226 | lines.some(
227 | (line) =>
228 | line.trim().length > 20 &&
229 | line.trim().length < 100 &&
230 | !line.startsWith("#"),
231 | ),
232 | severity: "critical",
233 | description: "Brief one-line description of what the project does",
234 | example:
235 | "> A fast, lightweight JavaScript framework for building web applications",
236 | });
237 |
238 | checklist.push({
239 | category: "Essential Sections",
240 | item: "Installation Instructions",
241 | present:
242 | /install/i.test(lowerContent) &&
243 | /npm|yarn|pip|cargo|go get|git clone/i.test(lowerContent),
244 | severity: "critical",
245 | description: "Clear installation or setup instructions",
246 | example: "```bash\nnpm install package-name\n```",
247 | });
248 |
249 | checklist.push({
250 | category: "Essential Sections",
251 | item: "Basic Usage Example",
252 | present:
253 | /usage|example|quick start|getting started/i.test(lowerContent) &&
254 | /```/.test(content),
255 | severity: "critical",
256 | description: "Working code example showing basic usage",
257 | example:
258 | '```javascript\nconst lib = require("package-name");\nlib.doSomething();\n```',
259 | });
260 |
261 | // Important Sections
262 | checklist.push({
263 | category: "Important Sections",
264 | item: "Prerequisites/Requirements",
265 | present:
266 | /prerequisite|requirement|dependencies|node|python|java|version/i.test(
267 | lowerContent,
268 | ),
269 | severity: "important",
270 | description: "Clear system requirements and dependencies",
271 | example: "- Node.js 16+\n- Docker (optional)",
272 | });
273 |
274 | checklist.push({
275 | category: "Important Sections",
276 | item: "License Information",
277 | present:
278 | /license/i.test(lowerContent) || /mit|apache|gpl|bsd/i.test(lowerContent),
279 | severity: "important",
280 | description: "Clear license information",
281 | example: "## License\n\nMIT License - see [LICENSE](LICENSE) file",
282 | });
283 |
284 | checklist.push({
285 | category: "Important Sections",
286 | item: "Contributing Guidelines",
287 | present: /contribut/i.test(lowerContent),
288 | severity: "important",
289 | description: "Information on how to contribute to the project",
290 | example: "See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines",
291 | });
292 |
293 | // Community Health
294 | checklist.push({
295 | category: "Community Health",
296 | item: "Code of Conduct",
297 | present: /code of conduct/i.test(lowerContent),
298 | severity: "recommended",
299 | description: "Link to code of conduct for community projects",
300 | example: "Please read our [Code of Conduct](CODE_OF_CONDUCT.md)",
301 | });
302 |
303 | checklist.push({
304 | category: "Community Health",
305 | item: "Issue Templates",
306 | present: /issue template|bug report|feature request/i.test(lowerContent),
307 | severity: "recommended",
308 | description: "Reference to issue templates for better bug reports",
309 | example:
310 | "Use our [issue templates](.github/ISSUE_TEMPLATE/) when reporting bugs",
311 | });
312 |
313 | // Visual Elements
314 | checklist.push({
315 | category: "Visual Elements",
316 | item: "Badges",
317 | present:
318 | /\[!\[.*\]\(.*\)\]\(.*\)/.test(content) || /badge/i.test(lowerContent),
319 | severity: "recommended",
320 | description: "Status badges for build, version, license, etc.",
321 | example: "[](link-url)",
322 | });
323 |
324 | checklist.push({
325 | category: "Visual Elements",
326 | item: "Screenshots/Demo",
327 | present:
328 | /!\[.*\]\(.*\.(png|jpg|jpeg|gif|webp)\)/i.test(content) ||
329 | /screenshot|demo|gif/i.test(lowerContent),
330 | severity:
331 | projectType === "application" || projectType === "tool"
332 | ? "important"
333 | : "recommended",
334 | description:
335 | "Visual demonstration of the project (especially for applications)",
336 | example: "",
337 | });
338 |
339 | // Content Quality
340 | checklist.push({
341 | category: "Content Quality",
342 | item: "Appropriate Length",
343 | present: lines.length >= 20 && lines.length <= 300,
344 | severity: "important",
345 | description:
346 | "README length appropriate for project complexity (20-300 lines)",
347 | example: "Keep main README focused, link to detailed docs",
348 | });
349 |
350 | checklist.push({
351 | category: "Content Quality",
352 | item: "Clear Section Headers",
353 | present: (content.match(/^##\s+/gm) || []).length >= 3,
354 | severity: "important",
355 | description: "Well-organized content with clear section headers",
356 | example: "## Installation\n## Usage\n## Contributing",
357 | });
358 |
359 | checklist.push({
360 | category: "Content Quality",
361 | item: "Working Links",
362 | present: !/\[.*\]\(\)/.test(content) && !/\[.*\]\(#\)/.test(content),
363 | severity: "important",
364 | description:
365 | "All links should be functional (no empty or placeholder links)",
366 | example: "[Documentation](https://example.com/docs)",
367 | });
368 |
369 | // Project-specific checks
370 | if (projectType === "library" || projectType === "framework") {
371 | checklist.push({
372 | category: "Library Specific",
373 | item: "API Documentation",
374 | present: /api|methods|functions|reference/i.test(lowerContent),
375 | severity: "important",
376 | description: "API documentation or link to detailed API reference",
377 | example:
378 | "See [API Documentation](docs/api.md) for detailed method reference",
379 | });
380 | }
381 |
382 | if (projectType === "application" || projectType === "tool") {
383 | checklist.push({
384 | category: "Application Specific",
385 | item: "Configuration Options",
386 | present: /config|settings|options|environment/i.test(lowerContent),
387 | severity: "important",
388 | description: "Configuration and customization options",
389 | example: "See [Configuration Guide](docs/configuration.md)",
390 | });
391 | }
392 |
393 | return checklist;
394 | }
395 |
396 | function calculateOverallScore(checklist: ChecklistItem[]): {
397 | score: number;
398 | grade: string;
399 | } {
400 | const weights = { critical: 3, important: 2, recommended: 1 };
401 | let totalScore = 0;
402 | let maxScore = 0;
403 |
404 | checklist.forEach((item) => {
405 | const weight = weights[item.severity];
406 | maxScore += weight;
407 | if (item.present) {
408 | totalScore += weight;
409 | }
410 | });
411 |
412 | const percentage =
413 | maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0;
414 |
415 | let grade: string;
416 | if (percentage >= 90) grade = "A";
417 | else if (percentage >= 80) grade = "B";
418 | else if (percentage >= 70) grade = "C";
419 | else if (percentage >= 60) grade = "D";
420 | else grade = "F";
421 |
422 | return { score: percentage, grade };
423 | }
424 |
425 | function generateRecommendations(
426 | checklist: ChecklistItem[],
427 | projectType: string,
428 | targetAudience: string,
429 | ): string[] {
430 | const recommendations: string[] = [];
431 | const missing = checklist.filter((item) => !item.present);
432 |
433 | // Critical issues first
434 | const critical = missing.filter((item) => item.severity === "critical");
435 | if (critical.length > 0) {
436 | recommendations.push(
437 | `🚨 Critical: Fix ${critical.length} essential sections: ${critical
438 | .map((item) => item.item)
439 | .join(", ")}`,
440 | );
441 | }
442 |
443 | // Important issues
444 | const important = missing.filter((item) => item.severity === "important");
445 | if (important.length > 0) {
446 | recommendations.push(
447 | `⚠️ Important: Add ${important.length} key sections: ${important
448 | .map((item) => item.item)
449 | .join(", ")}`,
450 | );
451 | }
452 |
453 | // Project-specific recommendations
454 | if (projectType === "library") {
455 | recommendations.push(
456 | "📚 Library Focus: Emphasize installation, basic usage, and API documentation",
457 | );
458 | } else if (projectType === "application") {
459 | recommendations.push(
460 | "🖥️ Application Focus: Include screenshots, configuration options, and deployment guides",
461 | );
462 | }
463 |
464 | // Target audience specific recommendations
465 | if (targetAudience === "beginner") {
466 | recommendations.push(
467 | "👶 Beginner-Friendly: Use simple language, provide detailed examples, include troubleshooting",
468 | );
469 | } else if (targetAudience === "advanced") {
470 | recommendations.push(
471 | "🎯 Advanced Users: Focus on technical details, performance notes, and extensibility",
472 | );
473 | }
474 |
475 | // General improvements
476 | const recommended = missing.filter((item) => item.severity === "recommended");
477 | if (recommended.length > 0) {
478 | recommendations.push(
479 | `✨ Enhancement: Consider adding ${recommended
480 | .map((item) => item.item)
481 | .join(", ")}`,
482 | );
483 | }
484 |
485 | return recommendations;
486 | }
487 |
488 | function generateTemplates(
489 | projectType: string,
490 | _generateTemplate: boolean,
491 | ): Record<string, string> {
492 | const templates: Record<string, string> = {};
493 |
494 | if (projectType === "library") {
495 | templates["README-library.md"] = `# Project Name
496 |
497 | > One-line description of what this library does
498 |
499 | [![Build Status][build-badge]][build-link]
500 | [![npm version][npm-badge]][npm-link]
501 | [![License][license-badge]][license-link]
502 |
503 | ## TL;DR
504 |
505 | What it does in 2-3 sentences. Who should use it.
506 |
507 | ## Quick Start
508 |
509 | ### Install
510 | \`\`\`bash
511 | npm install package-name
512 | \`\`\`
513 |
514 | ### Use
515 | \`\`\`javascript
516 | const lib = require('package-name');
517 |
518 | // Basic usage example
519 | const result = lib.doSomething();
520 | console.log(result);
521 | \`\`\`
522 |
523 | ## When to Use This
524 |
525 | - ✅ When you need X functionality
526 | - ✅ When you want Y capability
527 | - ❌ When you need Z (use [alternative] instead)
528 |
529 | ## API Reference
530 |
531 | ### \`doSomething(options)\`
532 |
533 | Description of the main method.
534 |
535 | **Parameters:**
536 | - \`options\` (Object): Configuration options
537 | - \`param1\` (string): Description of parameter
538 | - \`param2\` (boolean, optional): Description of optional parameter
539 |
540 | **Returns:** Description of return value
541 |
542 | **Example:**
543 | \`\`\`javascript
544 | const result = lib.doSomething({
545 | param1: 'value',
546 | param2: true
547 | });
548 | \`\`\`
549 |
550 | ## Full Documentation
551 |
552 | [Link to full documentation](docs/)
553 |
554 | ## Contributing
555 |
556 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
557 |
558 | ## License
559 |
560 | MIT License - see [LICENSE](LICENSE) file for details.
561 |
562 | [build-badge]: https://github.com/username/repo/workflows/CI/badge.svg
563 | [build-link]: https://github.com/username/repo/actions
564 | [npm-badge]: https://img.shields.io/npm/v/package-name.svg
565 | [npm-link]: https://www.npmjs.com/package/package-name
566 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
567 | [license-link]: LICENSE
568 | `;
569 | }
570 |
571 | if (projectType === "application" || projectType === "tool") {
572 | templates["README-application.md"] = `# Project Name
573 |
574 | > One-line description of what this application does
575 |
576 | 
577 |
578 | ## What This Does
579 |
580 | Brief explanation of the application's purpose and key features:
581 |
582 | - 🚀 Feature 1: Description
583 | - 📊 Feature 2: Description
584 | - 🔧 Feature 3: Description
585 |
586 | ## Quick Start
587 |
588 | ### Prerequisites
589 | - Node.js 16+
590 | - Docker (optional)
591 | - Other requirements
592 |
593 | ### Install & Run
594 | \`\`\`bash
595 | git clone https://github.com/username/repo.git
596 | cd project-name
597 | npm install
598 | npm start
599 | \`\`\`
600 |
601 | Visit \`http://localhost:3000\` to see the application.
602 |
603 | ## Configuration
604 |
605 | ### Environment Variables
606 | \`\`\`bash
607 | # Copy example config
608 | cp .env.example .env
609 |
610 | # Edit configuration
611 | nano .env
612 | \`\`\`
613 |
614 | ### Key Settings
615 | - \`PORT\`: Server port (default: 3000)
616 | - \`DATABASE_URL\`: Database connection string
617 | - \`API_KEY\`: External service API key
618 |
619 | ## Usage Examples
620 |
621 | ### Basic Usage
622 | \`\`\`bash
623 | npm run command -- --option value
624 | \`\`\`
625 |
626 | ### Advanced Usage
627 | \`\`\`bash
628 | npm run command -- --config custom.json --verbose
629 | \`\`\`
630 |
631 | ## Deployment
632 |
633 | See [Deployment Guide](docs/deployment.md) for production setup.
634 |
635 | ## Troubleshooting
636 |
637 | ### Common Issues
638 |
639 | **Issue 1: Error message**
640 | - Solution: Steps to resolve
641 |
642 | **Issue 2: Another error**
643 | - Solution: Steps to resolve
644 |
645 | See [FAQ](docs/FAQ.md) for more help.
646 |
647 | ## Contributing
648 |
649 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
650 |
651 | ## License
652 |
653 | MIT License - see [LICENSE](LICENSE) file for details.
654 | `;
655 | }
656 |
657 | return templates;
658 | }
659 |
660 | function generateCommunityFiles(_projectType: string): Record<string, string> {
661 | const files: Record<string, string> = {};
662 |
663 | files["CONTRIBUTING.md"] = `# Contributing to Project Name
664 |
665 | Thank you for your interest in contributing! This document provides guidelines for contributing to this project.
666 |
667 | ## Getting Started
668 |
669 | 1. Fork the repository
670 | 2. Clone your fork: \`git clone https://github.com/yourusername/repo.git\`
671 | 3. Create a feature branch: \`git checkout -b feature-name\`
672 | 4. Make your changes
673 | 5. Test your changes: \`npm test\`
674 | 6. Commit your changes: \`git commit -m "Description of changes"\`
675 | 7. Push to your fork: \`git push origin feature-name\`
676 | 8. Create a Pull Request
677 |
678 | ## Development Setup
679 |
680 | \`\`\`bash
681 | npm install
682 | npm run dev
683 | \`\`\`
684 |
685 | ## Code Style
686 |
687 | - Use TypeScript for new code
688 | - Follow existing code formatting
689 | - Run \`npm run lint\` before committing
690 | - Add tests for new features
691 |
692 | ## Pull Request Guidelines
693 |
694 | - Keep PRs focused and small
695 | - Include tests for new functionality
696 | - Update documentation as needed
697 | - Ensure CI passes
698 | - Link to relevant issues
699 |
700 | ## Reporting Issues
701 |
702 | Use our [issue templates](.github/ISSUE_TEMPLATE/) when reporting bugs or requesting features.
703 |
704 | ## Code of Conduct
705 |
706 | Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
707 | `;
708 |
709 | files["CODE_OF_CONDUCT.md"] = `# Code of Conduct
710 |
711 | ## Our Pledge
712 |
713 | We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
714 |
715 | ## Our Standards
716 |
717 | Examples of behavior that contributes to creating a positive environment include:
718 |
719 | - Using welcoming and inclusive language
720 | - Being respectful of differing viewpoints and experiences
721 | - Gracefully accepting constructive criticism
722 | - Focusing on what is best for the community
723 | - Showing empathy towards other community members
724 |
725 | Examples of unacceptable behavior include:
726 |
727 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
728 | - Trolling, insulting/derogatory comments, and personal or political attacks
729 | - Public or private harassment
730 | - Publishing others' private information without explicit permission
731 | - Other conduct which could reasonably be considered inappropriate in a professional setting
732 |
733 | ## Enforcement
734 |
735 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
736 |
737 | ## Attribution
738 |
739 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4.
740 | `;
741 |
742 | files["SECURITY.md"] = `# Security Policy
743 |
744 | ## Supported Versions
745 |
746 | | Version | Supported |
747 | | ------- | ------------------ |
748 | | 1.x.x | :white_check_mark: |
749 | | < 1.0 | :x: |
750 |
751 | ## Reporting a Vulnerability
752 |
753 | If you discover a security vulnerability, please report it privately:
754 |
755 | 1. **Do not** create a public issue
756 | 2. Email [email protected] with details
757 | 3. Include steps to reproduce if possible
758 | 4. We will respond within 48 hours
759 |
760 | ## Security Best Practices
761 |
762 | When using this project:
763 |
764 | - Keep dependencies updated
765 | - Use environment variables for secrets
766 | - Follow principle of least privilege
767 | - Regularly audit your setup
768 |
769 | Thank you for helping keep our project secure!
770 | `;
771 |
772 | return files;
773 | }
774 |
775 | async function writeGeneratedFiles(
776 | templates: Record<string, string>,
777 | communityFiles: Record<string, string>,
778 | outputDirectory: string,
779 | _originalReadmePath: string,
780 | ): Promise<void> {
781 | try {
782 | // Create output directory
783 | await mkdir(outputDirectory, { recursive: true });
784 |
785 | // Write templates
786 | for (const [filename, content] of Object.entries(templates)) {
787 | const filePath = join(outputDirectory, filename);
788 | await writeFile(filePath, content, "utf-8");
789 | }
790 |
791 | // Write community files
792 | for (const [filename, content] of Object.entries(communityFiles)) {
793 | const filePath = join(outputDirectory, filename);
794 | await writeFile(filePath, content, "utf-8");
795 | }
796 |
797 | // Create .github directory structure
798 | const githubDir = join(outputDirectory, ".github");
799 | await mkdir(githubDir, { recursive: true });
800 |
801 | const issueTemplateDir = join(githubDir, "ISSUE_TEMPLATE");
802 | await mkdir(issueTemplateDir, { recursive: true });
803 |
804 | // Bug report template
805 | const bugReportTemplate = `---
806 | name: Bug report
807 | about: Create a report to help us improve
808 | title: '[BUG] '
809 | labels: bug
810 | assignees: ''
811 | ---
812 |
813 | **Describe the bug**
814 | A clear and concise description of what the bug is.
815 |
816 | **To Reproduce**
817 | Steps to reproduce the behavior:
818 | 1. Go to '...'
819 | 2. Click on '....'
820 | 3. Scroll down to '....'
821 | 4. See error
822 |
823 | **Expected behavior**
824 | A clear and concise description of what you expected to happen.
825 |
826 | **Screenshots**
827 | If applicable, add screenshots to help explain your problem.
828 |
829 | **Environment:**
830 | - OS: [e.g. iOS]
831 | - Browser [e.g. chrome, safari]
832 | - Version [e.g. 22]
833 |
834 | **Additional context**
835 | Add any other context about the problem here.
836 | `;
837 |
838 | await writeFile(
839 | join(issueTemplateDir, "bug_report.yml"),
840 | bugReportTemplate,
841 | "utf-8",
842 | );
843 |
844 | // Feature request template
845 | const featureRequestTemplate = `---
846 | name: Feature request
847 | about: Suggest an idea for this project
848 | title: '[FEATURE] '
849 | labels: enhancement
850 | assignees: ''
851 | ---
852 |
853 | **Is your feature request related to a problem? Please describe.**
854 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
855 |
856 | **Describe the solution you'd like**
857 | A clear and concise description of what you want to happen.
858 |
859 | **Describe alternatives you've considered**
860 | A clear and concise description of any alternative solutions or features you've considered.
861 |
862 | **Additional context**
863 | Add any other context or screenshots about the feature request here.
864 | `;
865 |
866 | await writeFile(
867 | join(issueTemplateDir, "feature_request.yml"),
868 | featureRequestTemplate,
869 | "utf-8",
870 | );
871 |
872 | // Pull request template
873 | const prTemplate = `## Description
874 | Brief description of changes made.
875 |
876 | ## Type of Change
877 | - [ ] Bug fix (non-breaking change which fixes an issue)
878 | - [ ] New feature (non-breaking change which adds functionality)
879 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
880 | - [ ] Documentation update
881 |
882 | ## Testing
883 | - [ ] Tests pass locally
884 | - [ ] New tests added for new functionality
885 | - [ ] Manual testing completed
886 |
887 | ## Checklist
888 | - [ ] Code follows project style guidelines
889 | - [ ] Self-review completed
890 | - [ ] Documentation updated
891 | - [ ] No new warnings introduced
892 | `;
893 |
894 | await writeFile(
895 | join(githubDir, "PULL_REQUEST_TEMPLATE.md"),
896 | prTemplate,
897 | "utf-8",
898 | );
899 | } catch (error) {
900 | throw new Error(
901 | `Failed to write generated files: ${
902 | error instanceof Error ? error.message : "Unknown error"
903 | }`,
904 | );
905 | }
906 | }
907 |
908 | function calculateSummaryMetrics(checklist: ChecklistItem[]) {
909 | const criticalIssues = checklist.filter(
910 | (item) => !item.present && item.severity === "critical",
911 | ).length;
912 | const importantIssues = checklist.filter(
913 | (item) => !item.present && item.severity === "important",
914 | ).length;
915 | const recommendedImprovements = checklist.filter(
916 | (item) => !item.present && item.severity === "recommended",
917 | ).length;
918 | const sectionsPresent = checklist.filter((item) => item.present).length;
919 | const totalSections = checklist.length;
920 |
921 | // Estimate improvement time based on missing items
922 | const totalMissing =
923 | criticalIssues + importantIssues + recommendedImprovements;
924 | let estimatedTime = "";
925 | if (totalMissing === 0) {
926 | estimatedTime = "No improvements needed";
927 | } else if (totalMissing <= 3) {
928 | estimatedTime = "30 minutes - 1 hour";
929 | } else if (totalMissing <= 6) {
930 | estimatedTime = "1-2 hours";
931 | } else if (totalMissing <= 10) {
932 | estimatedTime = "2-4 hours";
933 | } else {
934 | estimatedTime = "4+ hours (consider phased approach)";
935 | }
936 |
937 | return {
938 | criticalIssues,
939 | importantIssues,
940 | recommendedImprovements,
941 | sectionsPresent,
942 | totalSections,
943 | estimatedImprovementTime: estimatedTime,
944 | };
945 | }
946 |
947 | function generateNextSteps(
948 | checklist: ChecklistItem[],
949 | generateTemplate: boolean,
950 | outputDirectory?: string,
951 | ): string[] {
952 | const nextSteps: string[] = [];
953 | const missing = checklist.filter((item) => !item.present);
954 |
955 | if (missing.length === 0) {
956 | nextSteps.push(
957 | "✅ README follows all best practices - no immediate action needed",
958 | );
959 | nextSteps.push(
960 | "📊 Consider periodic reviews to maintain quality as project evolves",
961 | );
962 | return nextSteps;
963 | }
964 |
965 | // Critical issues first
966 | const critical = missing.filter((item) => item.severity === "critical");
967 | if (critical.length > 0) {
968 | nextSteps.push(
969 | `🚨 Priority 1: Address ${critical.length} critical issues immediately`,
970 | );
971 | critical.forEach((item) => {
972 | nextSteps.push(` • Add ${item.item}: ${item.description}`);
973 | });
974 | }
975 |
976 | // Important issues
977 | const important = missing.filter((item) => item.severity === "important");
978 | if (important.length > 0) {
979 | nextSteps.push(
980 | `⚠️ Priority 2: Address ${important.length} important sections within 1 week`,
981 | );
982 | }
983 |
984 | // Template usage
985 | if (generateTemplate && outputDirectory) {
986 | nextSteps.push(`📝 Review generated templates in ${outputDirectory}/`);
987 | nextSteps.push("🔄 Customize templates to match your project specifics");
988 | nextSteps.push(
989 | "📋 Use community files (.github templates, CONTRIBUTING.md) to improve project health",
990 | );
991 | }
992 |
993 | // General improvements
994 | nextSteps.push(
995 | "🔍 Run this analysis periodically to maintain README quality",
996 | );
997 | nextSteps.push(
998 | "👥 Consider getting feedback from new users on README clarity",
999 | );
1000 |
1001 | return nextSteps;
1002 | }
1003 |
```