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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/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&amp;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: "[![Build Status](badge-url)](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: "![Demo](demo.gif)",
 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 | ![Demo](demo.gif)
 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 | 
```

--------------------------------------------------------------------------------
/src/tools/recommend-ssg.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { MCPToolResponse, formatMCPResponse } from "../types/api.js";
  3 | import {
  4 |   getKnowledgeGraph,
  5 |   getProjectContext,
  6 |   getMemoryManager,
  7 | } from "../memory/kg-integration.js";
  8 | import { getUserPreferenceManager } from "../memory/user-preferences.js";
  9 | 
 10 | // SSG scoring matrix based on ADR-003
 11 | export interface SSGRecommendation {
 12 |   recommended: "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy";
 13 |   confidence: number;
 14 |   reasoning: string[];
 15 |   alternatives: Array<{
 16 |     name: string;
 17 |     score: number;
 18 |     pros: string[];
 19 |     cons: string[];
 20 |   }>;
 21 |   historicalData?: {
 22 |     similarProjectCount: number;
 23 |     successRates: Record<string, { rate: number; sampleSize: number }>;
 24 |     topPerformer?: {
 25 |       ssg: string;
 26 |       successRate: number;
 27 |       deploymentCount: number;
 28 |     };
 29 |   };
 30 | }
 31 | 
 32 | const inputSchema = z.object({
 33 |   analysisId: z.string(),
 34 |   userId: z.string().optional().default("default"),
 35 |   preferences: z
 36 |     .object({
 37 |       priority: z.enum(["simplicity", "features", "performance"]).optional(),
 38 |       ecosystem: z
 39 |         .enum(["javascript", "python", "ruby", "go", "any"])
 40 |         .optional(),
 41 |     })
 42 |     .optional(),
 43 | });
 44 | 
 45 | /**
 46 |  * Phase 2.1: Retrieve historical deployment data from knowledge graph
 47 |  */
 48 | async function getHistoricalDeploymentData(
 49 |   projectPath?: string,
 50 |   technologies?: string[],
 51 | ): Promise<{
 52 |   similarProjectCount: number;
 53 |   successRates: Record<string, { rate: number; sampleSize: number }>;
 54 |   topPerformer?: {
 55 |     ssg: string;
 56 |     successRate: number;
 57 |     deploymentCount: number;
 58 |   };
 59 |   globalTopPerformer?: {
 60 |     ssg: string;
 61 |     successRate: number;
 62 |     deploymentCount: number;
 63 |   };
 64 | }> {
 65 |   try {
 66 |     const kg = await getKnowledgeGraph();
 67 | 
 68 |     // Get ALL projects for finding global top performers
 69 |     const allProjects = await kg.findNodes({ type: "project" });
 70 | 
 71 |     // Find similar projects (either by path or by shared technologies)
 72 |     let similarProjects = allProjects;
 73 | 
 74 |     if (projectPath) {
 75 |       // Get context for current project
 76 |       const context = await getProjectContext(projectPath);
 77 | 
 78 |       // If project exists in KG, use its similar projects
 79 |       if (context.similarProjects.length > 0) {
 80 |         similarProjects = context.similarProjects;
 81 |       } else if (technologies && technologies.length > 0) {
 82 |         // Project doesn't exist yet, but we have technologies - find similar by tech
 83 |         const techSet = new Set(technologies.map((t) => t.toLowerCase()));
 84 |         const projectsWithTech = [] as typeof similarProjects;
 85 | 
 86 |         for (const project of allProjects) {
 87 |           const projectTechs = project.properties.technologies || [];
 88 |           const hasShared = projectTechs.some((t: string) =>
 89 |             techSet.has(t.toLowerCase()),
 90 |           );
 91 |           if (hasShared) {
 92 |             projectsWithTech.push(project);
 93 |           }
 94 |         }
 95 |         similarProjects = projectsWithTech;
 96 |       } else {
 97 |         // No project found and no technologies provided
 98 |         similarProjects = [];
 99 |       }
100 |     } else if (technologies && technologies.length > 0) {
101 |       // Filter by shared technologies
102 |       const techSet = new Set(technologies.map((t) => t.toLowerCase()));
103 |       const projectsWithTech = [] as typeof similarProjects;
104 | 
105 |       for (const project of allProjects) {
106 |         const projectTechs = project.properties.technologies || [];
107 |         const hasShared = projectTechs.some((t: string) =>
108 |           techSet.has(t.toLowerCase()),
109 |         );
110 |         if (hasShared) {
111 |           projectsWithTech.push(project);
112 |         }
113 |       }
114 |       similarProjects = projectsWithTech;
115 |     } else {
116 |       // No criteria provided
117 |       similarProjects = [];
118 |     }
119 | 
120 |     // Aggregate deployment data by SSG for similar projects
121 |     const ssgStats: Record<
122 |       string,
123 |       { successes: number; failures: number; total: number }
124 |     > = {};
125 | 
126 |     // Also track global stats across ALL projects for finding top performers
127 |     const globalSSGStats: Record<
128 |       string,
129 |       { successes: number; failures: number; total: number }
130 |     > = {};
131 | 
132 |     // Helper function to aggregate stats for a set of projects
133 |     const aggregateStats = async (projects: typeof allProjects) => {
134 |       const stats: Record<
135 |         string,
136 |         { successes: number; failures: number; total: number }
137 |       > = {};
138 | 
139 |       for (const project of projects) {
140 |         const allEdges = await kg.findEdges({ source: project.id });
141 |         const deployments = allEdges.filter(
142 |           (e) =>
143 |             e.type.startsWith("project_deployed_with") ||
144 |             e.properties.baseType === "project_deployed_with",
145 |         );
146 | 
147 |         for (const deployment of deployments) {
148 |           const allNodes = await kg.getAllNodes();
149 |           const configNode = allNodes.find((n) => n.id === deployment.target);
150 | 
151 |           if (configNode && configNode.type === "configuration") {
152 |             const ssg = configNode.properties.ssg;
153 |             if (!stats[ssg]) {
154 |               stats[ssg] = { successes: 0, failures: 0, total: 0 };
155 |             }
156 | 
157 |             stats[ssg].total++;
158 |             if (deployment.properties.success) {
159 |               stats[ssg].successes++;
160 |             } else {
161 |               stats[ssg].failures++;
162 |             }
163 |           }
164 |         }
165 |       }
166 |       return stats;
167 |     };
168 | 
169 |     // Aggregate for similar projects
170 |     Object.assign(ssgStats, await aggregateStats(similarProjects));
171 | 
172 |     // Aggregate for ALL projects (for global top performer)
173 |     Object.assign(globalSSGStats, await aggregateStats(allProjects));
174 | 
175 |     // Calculate success rates for similar projects
176 |     const successRates: Record<string, { rate: number; sampleSize: number }> =
177 |       {};
178 |     let topPerformer:
179 |       | { ssg: string; successRate: number; deploymentCount: number }
180 |       | undefined;
181 |     let maxRate = 0;
182 | 
183 |     for (const [ssg, stats] of Object.entries(ssgStats)) {
184 |       if (stats.total > 0) {
185 |         const rate = stats.successes / stats.total;
186 |         successRates[ssg] = {
187 |           rate,
188 |           sampleSize: stats.total,
189 |         };
190 | 
191 |         // Track top performer in similar projects (require at least 2 deployments)
192 |         if (stats.total >= 2 && rate > maxRate) {
193 |           maxRate = rate;
194 |           topPerformer = {
195 |             ssg,
196 |             successRate: rate,
197 |             deploymentCount: stats.total,
198 |           };
199 |         }
200 |       }
201 |     }
202 | 
203 |     // Calculate global top performer from ALL projects
204 |     let globalTopPerformer:
205 |       | { ssg: string; successRate: number; deploymentCount: number }
206 |       | undefined;
207 |     let globalMaxRate = 0;
208 | 
209 |     for (const [ssg, stats] of Object.entries(globalSSGStats)) {
210 |       if (stats.total >= 2) {
211 |         const rate = stats.successes / stats.total;
212 |         if (rate > globalMaxRate) {
213 |           globalMaxRate = rate;
214 |           globalTopPerformer = {
215 |             ssg,
216 |             successRate: rate,
217 |             deploymentCount: stats.total,
218 |           };
219 |         }
220 |       }
221 |     }
222 | 
223 |     return {
224 |       similarProjectCount: similarProjects.length,
225 |       successRates,
226 |       topPerformer,
227 |       globalTopPerformer,
228 |     };
229 |   } catch (error) {
230 |     console.warn("Failed to retrieve historical deployment data:", error);
231 |     return {
232 |       similarProjectCount: 0,
233 |       successRates: {},
234 |     };
235 |   }
236 | }
237 | 
238 | /**
239 |  * Recommends the optimal static site generator (SSG) for a project based on analysis and historical data.
240 |  *
241 |  * This function provides intelligent SSG recommendations by analyzing project characteristics,
242 |  * considering user preferences, and leveraging historical deployment data from the knowledge graph.
243 |  * It uses a multi-criteria decision analysis approach to score different SSGs and provide
244 |  * confidence-weighted recommendations with detailed reasoning.
245 |  *
246 |  * @param args - The input arguments for SSG recommendation
247 |  * @param args.analysisId - Unique identifier from a previous repository analysis
248 |  * @param args.userId - User identifier for personalized recommendations (defaults to "default")
249 |  * @param args.preferences - Optional user preferences for recommendation weighting
250 |  * @param args.preferences.priority - Priority focus: "simplicity", "features", or "performance"
251 |  * @param args.preferences.ecosystem - Preferred technology ecosystem: "javascript", "python", "ruby", "go", or "any"
252 |  *
253 |  * @returns Promise resolving to SSG recommendation results
254 |  * @returns content - Array containing the recommendation results in MCP tool response format
255 |  *
256 |  * @throws {Error} When the analysis ID is invalid or not found
257 |  * @throws {Error} When historical data cannot be retrieved
258 |  * @throws {Error} When recommendation scoring fails
259 |  *
260 |  * @example
261 |  * ```typescript
262 |  * // Basic recommendation
263 |  * const recommendation = await recommendSSG({
264 |  *   analysisId: "analysis_abc123_def456",
265 |  *   userId: "user123"
266 |  * });
267 |  *
268 |  * // With preferences
269 |  * const personalized = await recommendSSG({
270 |  *   analysisId: "analysis_abc123_def456",
271 |  *   userId: "user123",
272 |  *   preferences: {
273 |  *     priority: "performance",
274 |  *     ecosystem: "javascript"
275 |  *   }
276 |  * });
277 |  * ```
278 |  *
279 |  * @since 1.0.0
280 |  * @version 1.2.0 - Added historical data integration and user preferences
281 |  */
282 | export async function recommendSSG(
283 |   args: unknown,
284 |   context?: any,
285 | ): Promise<{ content: any[] }> {
286 |   const startTime = Date.now();
287 |   const { analysisId, userId, preferences } = inputSchema.parse(args);
288 | 
289 |   const prioritizeSimplicity = preferences?.priority === "simplicity";
290 |   const ecosystemPreference = preferences?.ecosystem;
291 | 
292 |   // Report initial progress
293 |   if (context?.meta?.progressToken) {
294 |     await context.meta.reportProgress?.({
295 |       progress: 0,
296 |       total: 100,
297 |     });
298 |   }
299 | 
300 |   await context?.info?.("🔍 Starting SSG recommendation engine...");
301 | 
302 |   // Phase 2.2: Get user preference manager
303 |   await context?.info?.(`👤 Loading preferences for user: ${userId}...`);
304 |   const userPreferenceManager = await getUserPreferenceManager(userId);
305 | 
306 |   if (context?.meta?.progressToken) {
307 |     await context.meta.reportProgress?.({
308 |       progress: 15,
309 |       total: 100,
310 |     });
311 |   }
312 | 
313 |   try {
314 |     // Try to retrieve analysis from memory
315 |     await context?.info?.(`📊 Retrieving analysis: ${analysisId}...`);
316 |     let analysisData = null;
317 |     try {
318 |       const manager = await getMemoryManager();
319 |       const analysis = await manager.recall(analysisId);
320 |       if (analysis && analysis.data) {
321 |         // Handle the wrapped content structure
322 |         if (analysis.data.content && Array.isArray(analysis.data.content)) {
323 |           // Extract the JSON from the first text content
324 |           const firstContent = analysis.data.content[0];
325 |           if (
326 |             firstContent &&
327 |             firstContent.type === "text" &&
328 |             firstContent.text
329 |           ) {
330 |             try {
331 |               analysisData = JSON.parse(firstContent.text);
332 |             } catch (parseError) {
333 |               // If parse fails, try the direct data
334 |               analysisData = analysis.data;
335 |             }
336 |           }
337 |         } else {
338 |           // Direct data structure
339 |           analysisData = analysis.data;
340 |         }
341 |       }
342 |     } catch (error) {
343 |       // If memory retrieval fails, continue with fallback logic
344 |       console.warn(
345 |         `Could not retrieve analysis ${analysisId} from memory:`,
346 |         error,
347 |       );
348 |     }
349 | 
350 |     if (context?.meta?.progressToken) {
351 |       await context.meta.reportProgress?.({
352 |         progress: 30,
353 |         total: 100,
354 |       });
355 |     }
356 | 
357 |     // Phase 2.1: Retrieve historical deployment data
358 |     await context?.info?.("📈 Analyzing historical deployment data...");
359 |     let historicalData:
360 |       | {
361 |           similarProjectCount: number;
362 |           successRates: Record<string, { rate: number; sampleSize: number }>;
363 |           topPerformer?: {
364 |             ssg: string;
365 |             successRate: number;
366 |             deploymentCount: number;
367 |           };
368 |           globalTopPerformer?: {
369 |             ssg: string;
370 |             successRate: number;
371 |             deploymentCount: number;
372 |           };
373 |         }
374 |       | undefined;
375 | 
376 |     if (analysisData) {
377 |       const projectPath = analysisData.path;
378 |       const technologies = analysisData.dependencies?.languages || [];
379 |       historicalData = await getHistoricalDeploymentData(
380 |         projectPath,
381 |         technologies,
382 |       );
383 | 
384 |       if (historicalData && historicalData.similarProjectCount > 0) {
385 |         await context?.info?.(
386 |           `✨ Found ${historicalData.similarProjectCount} similar project(s) with deployment history`,
387 |         );
388 |       }
389 |     }
390 | 
391 |     if (context?.meta?.progressToken) {
392 |       await context.meta.reportProgress?.({
393 |         progress: 50,
394 |         total: 100,
395 |       });
396 |     }
397 | 
398 |     await context?.info?.("🤔 Calculating SSG recommendations...");
399 | 
400 |     // Determine recommendation based on analysis data if available
401 |     let finalRecommendation:
402 |       | "jekyll"
403 |       | "hugo"
404 |       | "docusaurus"
405 |       | "mkdocs"
406 |       | "eleventy";
407 |     let reasoning: string[] = [];
408 |     let confidence = 0.85;
409 | 
410 |     if (analysisData) {
411 |       // Use actual analysis data to make informed recommendation
412 |       const ecosystem = analysisData.dependencies?.ecosystem || "unknown";
413 |       const hasReact = analysisData.dependencies?.packages?.some(
414 |         (p: string) => p.includes("react") || p.includes("next"),
415 |       );
416 |       const complexity =
417 |         analysisData.documentation?.estimatedComplexity || "moderate";
418 |       const teamSize = analysisData.recommendations?.teamSize || "small";
419 | 
420 |       // Logic based on real analysis
421 |       if (ecosystem === "python") {
422 |         finalRecommendation = "mkdocs";
423 |         reasoning = [
424 |           "Python ecosystem detected - MkDocs integrates naturally",
425 |           "Simple configuration with YAML",
426 |           "Material theme provides excellent UI out of the box",
427 |           "Strong Python community support",
428 |         ];
429 |       } else if (ecosystem === "ruby") {
430 |         finalRecommendation = "jekyll";
431 |         reasoning = [
432 |           "Ruby ecosystem detected - Jekyll is the native choice",
433 |           "GitHub Pages native support",
434 |           "Simple static site generation",
435 |           "Extensive theme ecosystem",
436 |         ];
437 |       } else if (hasReact || ecosystem === "javascript") {
438 |         if (complexity === "complex" || teamSize === "large") {
439 |           finalRecommendation = "docusaurus";
440 |           reasoning = [
441 |             "JavaScript/TypeScript ecosystem with React detected",
442 |             "Complex project structure benefits from Docusaurus features",
443 |             "Built-in versioning and internationalization",
444 |             "MDX support for interactive documentation",
445 |           ];
446 |         } else if (prioritizeSimplicity) {
447 |           finalRecommendation = "eleventy";
448 |           reasoning = [
449 |             "JavaScript ecosystem with simplicity priority",
450 |             "Minimal configuration required",
451 |             "Fast build times",
452 |             "Flexible templating options",
453 |           ];
454 |         } else {
455 |           finalRecommendation = "docusaurus";
456 |           reasoning = [
457 |             "JavaScript/TypeScript ecosystem detected",
458 |             "Modern React-based framework",
459 |             "Active community and regular updates",
460 |             "Great developer experience",
461 |           ];
462 |         }
463 |       } else if (ecosystem === "go") {
464 |         finalRecommendation = "hugo";
465 |         reasoning = [
466 |           "Go ecosystem detected - Hugo is written in Go",
467 |           "Extremely fast build times",
468 |           "No runtime dependencies",
469 |           "Excellent for large documentation sites",
470 |         ];
471 |       } else {
472 |         // Default logic when ecosystem is unknown
473 |         if (prioritizeSimplicity) {
474 |           finalRecommendation = "jekyll";
475 |           reasoning = [
476 |             "Simple setup and configuration",
477 |             "GitHub Pages native support",
478 |             "Extensive documentation and community",
479 |             "Mature and stable platform",
480 |           ];
481 |         } else {
482 |           finalRecommendation = "docusaurus";
483 |           reasoning = [
484 |             "Modern documentation framework",
485 |             "Rich feature set out of the box",
486 |             "Great for technical documentation",
487 |             "Active development and support",
488 |           ];
489 |         }
490 |       }
491 | 
492 |       // Apply preference overrides
493 |       if (ecosystemPreference && ecosystemPreference !== "any") {
494 |         if (ecosystemPreference === "python") {
495 |           finalRecommendation = "mkdocs";
496 |           reasoning.unshift("Python ecosystem explicitly requested");
497 |         } else if (ecosystemPreference === "ruby") {
498 |           finalRecommendation = "jekyll";
499 |           reasoning.unshift("Ruby ecosystem explicitly requested");
500 |         } else if (ecosystemPreference === "go") {
501 |           finalRecommendation = "hugo";
502 |           reasoning.unshift("Go ecosystem explicitly requested");
503 |         } else if (ecosystemPreference === "javascript") {
504 |           if (
505 |             finalRecommendation !== "docusaurus" &&
506 |             finalRecommendation !== "eleventy"
507 |           ) {
508 |             finalRecommendation = prioritizeSimplicity
509 |               ? "eleventy"
510 |               : "docusaurus";
511 |             reasoning.unshift("JavaScript ecosystem explicitly requested");
512 |           }
513 |         }
514 |       }
515 | 
516 |       // Adjust confidence based on data quality
517 |       if (analysisData.structure?.totalFiles > 100) {
518 |         confidence = Math.min(0.95, confidence + 0.05);
519 |       }
520 |       if (
521 |         analysisData.documentation?.hasReadme &&
522 |         analysisData.documentation?.hasDocs
523 |       ) {
524 |         confidence = Math.min(0.95, confidence + 0.05);
525 |       }
526 | 
527 |       // Phase 2.1: Adjust recommendation and confidence based on historical data
528 |       if (historicalData && historicalData.similarProjectCount >= 0) {
529 |         const recommendedSuccessRate =
530 |           historicalData.successRates[finalRecommendation];
531 | 
532 |         if (recommendedSuccessRate) {
533 |           // Boost confidence if historically successful
534 |           if (recommendedSuccessRate.rate >= 1.0) {
535 |             // Perfect success rate - maximum boost
536 |             confidence = Math.min(0.98, confidence + 0.2);
537 |             reasoning.unshift(
538 |               `✅ 100% success rate in ${recommendedSuccessRate.sampleSize} similar project(s)`,
539 |             );
540 |           } else if (
541 |             recommendedSuccessRate.rate > 0.8 &&
542 |             recommendedSuccessRate.sampleSize >= 2
543 |           ) {
544 |             // High success rate - good boost
545 |             confidence = Math.min(0.98, confidence + 0.15);
546 |             reasoning.unshift(
547 |               `✅ ${(recommendedSuccessRate.rate * 100).toFixed(
548 |                 0,
549 |               )}% success rate in ${
550 |                 recommendedSuccessRate.sampleSize
551 |               } similar project(s)`,
552 |             );
553 |           } else if (
554 |             recommendedSuccessRate.rate < 0.5 &&
555 |             recommendedSuccessRate.sampleSize >= 2
556 |           ) {
557 |             // Reduce confidence if historically problematic
558 |             confidence = Math.max(0.5, confidence - 0.15);
559 |             reasoning.unshift(
560 |               `⚠️ Only ${(recommendedSuccessRate.rate * 100).toFixed(
561 |                 0,
562 |               )}% success rate in ${
563 |                 recommendedSuccessRate.sampleSize
564 |               } similar project(s)`,
565 |             );
566 |           }
567 |         } else {
568 |           // No deployment history for recommended SSG
569 |           // Check if similar projects had poor outcomes with OTHER SSGs
570 |           // This indicates general deployment challenges
571 |           const allSuccessRates = Object.values(historicalData.successRates);
572 |           if (allSuccessRates.length > 0) {
573 |             const avgSuccessRate =
574 |               allSuccessRates.reduce((sum, data) => sum + data.rate, 0) /
575 |               allSuccessRates.length;
576 |             const totalSamples = allSuccessRates.reduce(
577 |               (sum, data) => sum + data.sampleSize,
578 |               0,
579 |             );
580 | 
581 |             // If similar projects had poor deployment success overall, reduce confidence
582 |             if (avgSuccessRate < 0.5 && totalSamples >= 2) {
583 |               confidence = Math.max(0.6, confidence - 0.2);
584 |               // Find the SSG with worst performance to mention
585 |               const worstSSG = Object.entries(
586 |                 historicalData.successRates,
587 |               ).reduce(
588 |                 (worst, [ssg, data]) =>
589 |                   data.rate < worst.rate ? { ssg, rate: data.rate } : worst,
590 |                 { ssg: "", rate: 1.0 },
591 |               );
592 |               reasoning.unshift(
593 |                 `⚠️ Similar projects had deployment challenges (${
594 |                   worstSSG.ssg
595 |                 }: ${(worstSSG.rate * 100).toFixed(0)}% success rate)`,
596 |               );
597 |             }
598 |           }
599 |         }
600 | 
601 |         // Consider switching to top performer if significantly better
602 |         // Prefer similar project top performer, fall back to global top performer
603 |         const performerToConsider =
604 |           historicalData.topPerformer || historicalData.globalTopPerformer;
605 | 
606 |         if (
607 |           performerToConsider &&
608 |           performerToConsider.ssg !== finalRecommendation
609 |         ) {
610 |           const topPerformer = performerToConsider;
611 |           const currentRate = recommendedSuccessRate?.rate || 0.5;
612 |           const isFromSimilarProjects = !!historicalData.topPerformer;
613 | 
614 |           // Only switch if from similar projects (same ecosystem/technologies)
615 |           // For cross-ecosystem recommendations, just mention as alternative
616 |           const shouldSwitch =
617 |             isFromSimilarProjects &&
618 |             topPerformer.successRate > currentRate + 0.2 &&
619 |             topPerformer.deploymentCount >= 2;
620 | 
621 |           const shouldMention =
622 |             !shouldSwitch &&
623 |             topPerformer.successRate >= 0.8 &&
624 |             topPerformer.deploymentCount >= 2;
625 | 
626 |           if (shouldSwitch) {
627 |             reasoning.unshift(
628 |               `📊 Switching to ${topPerformer.ssg} based on ${(
629 |                 topPerformer.successRate * 100
630 |               ).toFixed(0)}% success rate across ${
631 |                 topPerformer.deploymentCount
632 |               } deployments`,
633 |             );
634 |             finalRecommendation = topPerformer.ssg as
635 |               | "jekyll"
636 |               | "hugo"
637 |               | "docusaurus"
638 |               | "mkdocs"
639 |               | "eleventy";
640 |             confidence = Math.min(0.95, topPerformer.successRate + 0.1);
641 |           } else if (shouldMention) {
642 |             // Mention as alternative if it has good success rate
643 |             const projectScope = isFromSimilarProjects
644 |               ? "similar projects"
645 |               : "all projects";
646 |             reasoning.push(
647 |               `💡 Alternative: ${topPerformer.ssg} has ${(
648 |                 topPerformer.successRate * 100
649 |               ).toFixed(0)}% success rate in ${projectScope}`,
650 |             );
651 |           }
652 |         }
653 | 
654 |         // Add general historical context
655 |         if (historicalData.similarProjectCount >= 2) {
656 |           const totalDeployments = Object.values(
657 |             historicalData.successRates,
658 |           ).reduce((sum, data) => sum + data.sampleSize, 0);
659 |           reasoning.push(
660 |             `📚 Based on ${totalDeployments} deployment(s) across ${historicalData.similarProjectCount} similar project(s)`,
661 |           );
662 |         }
663 |       }
664 |     } else {
665 |       // Fallback logic when no analysis data is available
666 |       const baseRecommendation = prioritizeSimplicity ? "jekyll" : "docusaurus";
667 |       finalRecommendation =
668 |         ecosystemPreference === "python" ? "mkdocs" : baseRecommendation;
669 |       reasoning = [
670 |         "Recommendation based on preferences without full analysis",
671 |         "Consider running analyze_repository for more accurate recommendation",
672 |       ];
673 |       confidence = 0.65; // Lower confidence without analysis data
674 |     }
675 | 
676 |     // Phase 2.2: Apply user preferences to recommendation
677 |     // For preference checking, include all SSGs except the current recommendation
678 |     // This ensures user preferences can override even if their preferred SSG isn't in top alternatives
679 |     const allSSGs: Array<
680 |       "jekyll" | "hugo" | "docusaurus" | "mkdocs" | "eleventy"
681 |     > = ["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"];
682 |     const alternativeNames = allSSGs.filter(
683 |       (ssg) => ssg !== finalRecommendation,
684 |     );
685 | 
686 |     const preferenceAdjustment =
687 |       userPreferenceManager.applyPreferencesToRecommendation(
688 |         finalRecommendation,
689 |         alternativeNames,
690 |       );
691 | 
692 |     if (preferenceAdjustment.adjustmentReason) {
693 |       // User preferences led to a different recommendation
694 |       finalRecommendation = preferenceAdjustment.recommended as
695 |         | "jekyll"
696 |         | "hugo"
697 |         | "docusaurus"
698 |         | "mkdocs"
699 |         | "eleventy";
700 |       reasoning.unshift(`🎯 ${preferenceAdjustment.adjustmentReason}`);
701 |       confidence = Math.min(0.95, confidence + 0.05);
702 |     }
703 | 
704 |     const recommendation: SSGRecommendation = {
705 |       recommended: finalRecommendation,
706 |       confidence,
707 |       reasoning,
708 |       alternatives: getAlternatives(finalRecommendation, prioritizeSimplicity),
709 |       historicalData,
710 |     };
711 | 
712 |     if (context?.meta?.progressToken) {
713 |       await context.meta.reportProgress?.({
714 |         progress: 100,
715 |         total: 100,
716 |       });
717 |     }
718 | 
719 |     const executionTime = Date.now() - startTime;
720 |     await context?.info?.(
721 |       `✅ Recommendation complete! Suggesting ${recommendation.recommended.toUpperCase()} with ${(
722 |         recommendation.confidence * 100
723 |       ).toFixed(0)}% confidence (${Math.round(executionTime / 1000)}s)`,
724 |     );
725 | 
726 |     const response: MCPToolResponse<SSGRecommendation> = {
727 |       success: true,
728 |       data: recommendation,
729 |       metadata: {
730 |         toolVersion: "1.0.0",
731 |         executionTime,
732 |         timestamp: new Date().toISOString(),
733 |         analysisId,
734 |       },
735 |       recommendations: [
736 |         {
737 |           type: "info",
738 |           title: "SSG Recommendation",
739 |           description: `${recommendation.recommended} recommended with ${(
740 |             recommendation.confidence * 100
741 |           ).toFixed(0)}% confidence`,
742 |         },
743 |       ],
744 |       nextSteps: [
745 |         {
746 |           action: "Generate Configuration",
747 |           toolRequired: "generate_config",
748 |           description: `Create ${recommendation.recommended} configuration files`,
749 |           priority: "high",
750 |         },
751 |       ],
752 |     };
753 | 
754 |     return formatMCPResponse(response);
755 |   } catch (error) {
756 |     const errorResponse: MCPToolResponse = {
757 |       success: false,
758 |       error: {
759 |         code: "RECOMMENDATION_FAILED",
760 |         message: `Failed to generate SSG recommendation: ${error}`,
761 |         resolution:
762 |           "Ensure analysis ID is valid and preferences are correctly formatted",
763 |       },
764 |       metadata: {
765 |         toolVersion: "1.0.0",
766 |         executionTime: Date.now() - startTime,
767 |         timestamp: new Date().toISOString(),
768 |         analysisId,
769 |       },
770 |     };
771 |     return formatMCPResponse(errorResponse);
772 |   }
773 | }
774 | 
775 | function getAlternatives(
776 |   recommended: string,
777 |   prioritizeSimplicity: boolean,
778 | ): SSGRecommendation["alternatives"] {
779 |   const allSSGs = [
780 |     {
781 |       name: "Jekyll",
782 |       score: prioritizeSimplicity ? 0.85 : 0.7,
783 |       pros: [
784 |         "Simple setup",
785 |         "GitHub Pages native",
786 |         "Extensive themes",
787 |         "Ruby ecosystem",
788 |       ],
789 |       cons: [
790 |         "Ruby dependency",
791 |         "Slower builds for large sites",
792 |         "Limited dynamic features",
793 |       ],
794 |     },
795 |     {
796 |       name: "Hugo",
797 |       score: prioritizeSimplicity ? 0.65 : 0.75,
798 |       pros: [
799 |         "Extremely fast builds",
800 |         "No dependencies",
801 |         "Go templating",
802 |         "Great for large sites",
803 |       ],
804 |       cons: [
805 |         "Steeper learning curve",
806 |         "Go templating may be unfamiliar",
807 |         "Less flexible themes",
808 |       ],
809 |     },
810 |     {
811 |       name: "Docusaurus",
812 |       score: prioritizeSimplicity ? 0.7 : 0.9,
813 |       pros: [
814 |         "React-based",
815 |         "Rich features",
816 |         "MDX support",
817 |         "Built-in versioning",
818 |       ],
819 |       cons: [
820 |         "More complex setup",
821 |         "Node.js dependency",
822 |         "Heavier than static generators",
823 |       ],
824 |     },
825 |     {
826 |       name: "MkDocs",
827 |       score: prioritizeSimplicity ? 0.8 : 0.75,
828 |       pros: [
829 |         "Simple setup",
830 |         "Python-based",
831 |         "Great themes",
832 |         "Easy configuration",
833 |       ],
834 |       cons: [
835 |         "Python dependency",
836 |         "Less flexible than React-based",
837 |         "Limited customization",
838 |       ],
839 |     },
840 |     {
841 |       name: "Eleventy",
842 |       score: prioritizeSimplicity ? 0.75 : 0.7,
843 |       pros: [
844 |         "Minimal config",
845 |         "Fast builds",
846 |         "Flexible templates",
847 |         "JavaScript ecosystem",
848 |       ],
849 |       cons: [
850 |         "Less opinionated",
851 |         "Fewer built-in features",
852 |         "Requires more setup for complex sites",
853 |       ],
854 |     },
855 |   ];
856 | 
857 |   // Filter out the recommended SSG and sort by score
858 |   return allSSGs
859 |     .filter((ssg) => ssg.name.toLowerCase() !== recommended.toLowerCase())
860 |     .sort((a, b) => b.score - a.score)
861 |     .slice(0, 2); // Return top 2 alternatives
862 | }
863 | 
```

--------------------------------------------------------------------------------
/tests/functional/tools.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Functional tests for all MCP tools with real repository scenarios
  2 | import { promises as fs } from "fs";
  3 | import path from "path";
  4 | import os from "os";
  5 | import { analyzeRepository } from "../../src/tools/analyze-repository";
  6 | import { recommendSSG } from "../../src/tools/recommend-ssg";
  7 | import { generateConfig } from "../../src/tools/generate-config";
  8 | import { setupStructure } from "../../src/tools/setup-structure";
  9 | import { deployPages } from "../../src/tools/deploy-pages";
 10 | import { verifyDeployment } from "../../src/tools/verify-deployment";
 11 | 
 12 | describe("Functional Testing - MCP Tools", () => {
 13 |   let tempDir: string;
 14 |   let testRepos: {
 15 |     javascript: string;
 16 |     python: string;
 17 |     ruby: string;
 18 |     go: string;
 19 |     mixed: string;
 20 |     large: string;
 21 |     empty: string;
 22 |   };
 23 | 
 24 |   beforeAll(async () => {
 25 |     tempDir = path.join(os.tmpdir(), "documcp-functional-tests");
 26 |     await fs.mkdir(tempDir, { recursive: true });
 27 | 
 28 |     testRepos = {
 29 |       javascript: await createJavaScriptRepo(),
 30 |       python: await createPythonRepo(),
 31 |       ruby: await createRubyRepo(),
 32 |       go: await createGoRepo(),
 33 |       mixed: await createMixedLanguageRepo(),
 34 |       large: await createLargeRepo(),
 35 |       empty: await createEmptyRepo(),
 36 |     };
 37 |   });
 38 | 
 39 |   afterAll(async () => {
 40 |     try {
 41 |       await fs.rm(tempDir, { recursive: true, force: true });
 42 |     } catch (error) {
 43 |       console.warn("Failed to cleanup test directory:", error);
 44 |     }
 45 |   });
 46 | 
 47 |   describe("analyze_repository Tool", () => {
 48 |     it("should analyze JavaScript/TypeScript repository correctly", async () => {
 49 |       const result = await analyzeRepository({
 50 |         path: testRepos.javascript,
 51 |         depth: "standard",
 52 |       });
 53 | 
 54 |       expect(result.content).toBeDefined();
 55 |       expect(result.content.length).toBeGreaterThan(0);
 56 | 
 57 |       // Parse the JSON response to validate structure
 58 |       const analysisText = result.content.find((c) => c.text.includes('"id"'));
 59 |       expect(analysisText).toBeDefined();
 60 | 
 61 |       const analysis = JSON.parse(analysisText!.text);
 62 |       expect(analysis.dependencies.ecosystem).toBe("javascript");
 63 |       expect(analysis.structure.languages[".js"]).toBeGreaterThan(0);
 64 |       expect(analysis.documentation.hasReadme).toBe(true);
 65 |       expect(analysis.recommendations.primaryLanguage).toBe("javascript");
 66 |     });
 67 | 
 68 |     it("should analyze Python repository correctly", async () => {
 69 |       const result = await analyzeRepository({
 70 |         path: testRepos.python,
 71 |         depth: "standard",
 72 |       });
 73 | 
 74 |       const analysisText = result.content.find((c) =>
 75 |         c.text.includes('"ecosystem"'),
 76 |       );
 77 |       const analysis = JSON.parse(analysisText!.text);
 78 | 
 79 |       expect(analysis.dependencies.ecosystem).toBe("python");
 80 |       expect(analysis.structure.languages[".py"]).toBeGreaterThan(0);
 81 |       expect(analysis.dependencies.packages.length).toBeGreaterThan(0);
 82 |     });
 83 | 
 84 |     it("should analyze Ruby repository correctly", async () => {
 85 |       const result = await analyzeRepository({
 86 |         path: testRepos.ruby,
 87 |         depth: "standard",
 88 |       });
 89 | 
 90 |       const analysisText = result.content.find((c) =>
 91 |         c.text.includes('"ecosystem"'),
 92 |       );
 93 |       const analysis = JSON.parse(analysisText!.text);
 94 | 
 95 |       expect(analysis.dependencies.ecosystem).toBe("ruby");
 96 |       expect(analysis.structure.languages[".rb"]).toBeGreaterThan(0);
 97 |     });
 98 | 
 99 |     it("should analyze Go repository correctly", async () => {
100 |       const result = await analyzeRepository({
101 |         path: testRepos.go,
102 |         depth: "standard",
103 |       });
104 | 
105 |       const analysisText = result.content.find((c) =>
106 |         c.text.includes('"ecosystem"'),
107 |       );
108 |       const analysis = JSON.parse(analysisText!.text);
109 | 
110 |       expect(analysis.dependencies.ecosystem).toBe("go");
111 |       expect(analysis.structure.languages[".go"]).toBeGreaterThan(0);
112 |     });
113 | 
114 |     it("should handle different analysis depths", async () => {
115 |       const quickResult = await analyzeRepository({
116 |         path: testRepos.javascript,
117 |         depth: "quick",
118 |       });
119 | 
120 |       const deepResult = await analyzeRepository({
121 |         path: testRepos.javascript,
122 |         depth: "deep",
123 |       });
124 | 
125 |       expect(quickResult.content).toBeDefined();
126 |       expect(deepResult.content).toBeDefined();
127 | 
128 |       // Both should return valid results but potentially different detail levels
129 |       const quickAnalysis = JSON.parse(
130 |         quickResult.content.find((c) => c.text.includes('"id"'))!.text,
131 |       );
132 |       const deepAnalysis = JSON.parse(
133 |         deepResult.content.find((c) => c.text.includes('"id"'))!.text,
134 |       );
135 | 
136 |       expect(quickAnalysis.id).toBeDefined();
137 |       expect(deepAnalysis.id).toBeDefined();
138 |     });
139 | 
140 |     it("should handle empty repository gracefully", async () => {
141 |       const result = await analyzeRepository({
142 |         path: testRepos.empty,
143 |         depth: "standard",
144 |       });
145 | 
146 |       const analysisText = result.content.find((c) =>
147 |         c.text.includes('"totalFiles"'),
148 |       );
149 |       const analysis = JSON.parse(analysisText!.text);
150 | 
151 |       expect(analysis.structure.totalFiles).toBe(1); // Only README.md
152 |       expect(analysis.dependencies.ecosystem).toBe("unknown");
153 |     });
154 | 
155 |     it("should handle non-existent repository path", async () => {
156 |       const nonExistentPath = path.join(tempDir, "does-not-exist");
157 | 
158 |       const result = await analyzeRepository({
159 |         path: nonExistentPath,
160 |         depth: "standard",
161 |       });
162 | 
163 |       expect((result as any).isError).toBe(true);
164 |       expect(result.content[0].text).toContain("Error:");
165 |     });
166 |   });
167 | 
168 |   describe("recommend_ssg Tool", () => {
169 |     it("should recommend SSG based on analysis", async () => {
170 |       const result = await recommendSSG({
171 |         analysisId: "test-analysis-123",
172 |       });
173 | 
174 |       expect(result.content).toBeDefined();
175 |       expect(result.content.length).toBeGreaterThan(0);
176 | 
177 |       // Should contain recommendation data
178 |       const recommendationText = result.content.find((c) =>
179 |         c.text.includes('"recommended"'),
180 |       );
181 |       expect(recommendationText).toBeDefined();
182 | 
183 |       const recommendation = JSON.parse(recommendationText!.text);
184 |       expect(recommendation.recommended).toBeDefined();
185 |       expect(recommendation.confidence).toBeGreaterThan(0);
186 |       expect(recommendation.reasoning).toBeDefined();
187 |       expect(recommendation.alternatives).toBeDefined();
188 |     });
189 | 
190 |     it("should handle preferences parameter", async () => {
191 |       const result = await recommendSSG({
192 |         analysisId: "test-analysis-456",
193 |         preferences: {
194 |           priority: "simplicity",
195 |           ecosystem: "javascript",
196 |         },
197 |       });
198 | 
199 |       expect(result.content).toBeDefined();
200 |       const recommendationText = result.content.find((c) =>
201 |         c.text.includes('"recommended"'),
202 |       );
203 |       const recommendation = JSON.parse(recommendationText!.text);
204 | 
205 |       expect(["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"]).toContain(
206 |         recommendation.recommended,
207 |       );
208 |     });
209 |   });
210 | 
211 |   describe("generate_config Tool", () => {
212 |     let configOutputDir: string;
213 | 
214 |     beforeEach(async () => {
215 |       configOutputDir = path.join(
216 |         tempDir,
217 |         "config-output",
218 |         Date.now().toString(),
219 |       );
220 |       await fs.mkdir(configOutputDir, { recursive: true });
221 |     });
222 | 
223 |     it("should generate Docusaurus configuration", async () => {
224 |       const result = await generateConfig({
225 |         ssg: "docusaurus",
226 |         projectName: "Test Docusaurus Project",
227 |         projectDescription: "A test project for Docusaurus",
228 |         outputPath: configOutputDir,
229 |       });
230 | 
231 |       expect(result.content).toBeDefined();
232 | 
233 |       // Verify files were created
234 |       const docusaurusConfig = path.join(
235 |         configOutputDir,
236 |         "docusaurus.config.js",
237 |       );
238 |       const packageJson = path.join(configOutputDir, "package.json");
239 | 
240 |       expect(
241 |         await fs
242 |           .access(docusaurusConfig)
243 |           .then(() => true)
244 |           .catch(() => false),
245 |       ).toBe(true);
246 |       expect(
247 |         await fs
248 |           .access(packageJson)
249 |           .then(() => true)
250 |           .catch(() => false),
251 |       ).toBe(true);
252 | 
253 |       // Verify file contents
254 |       const configContent = await fs.readFile(docusaurusConfig, "utf-8");
255 |       expect(configContent).toContain("Test Docusaurus Project");
256 |       expect(configContent).toContain("classic");
257 |     });
258 | 
259 |     it("should generate MkDocs configuration", async () => {
260 |       const result = await generateConfig({
261 |         ssg: "mkdocs",
262 |         projectName: "Test MkDocs Project",
263 |         outputPath: configOutputDir,
264 |       });
265 | 
266 |       expect(result.content).toBeDefined();
267 | 
268 |       const mkdocsConfig = path.join(configOutputDir, "mkdocs.yml");
269 |       const requirements = path.join(configOutputDir, "requirements.txt");
270 | 
271 |       expect(
272 |         await fs
273 |           .access(mkdocsConfig)
274 |           .then(() => true)
275 |           .catch(() => false),
276 |       ).toBe(true);
277 |       expect(
278 |         await fs
279 |           .access(requirements)
280 |           .then(() => true)
281 |           .catch(() => false),
282 |       ).toBe(true);
283 | 
284 |       const configContent = await fs.readFile(mkdocsConfig, "utf-8");
285 |       expect(configContent).toContain("Test MkDocs Project");
286 |       expect(configContent).toContain("material");
287 |     });
288 | 
289 |     it("should generate Hugo configuration", async () => {
290 |       const result = await generateConfig({
291 |         ssg: "hugo",
292 |         projectName: "Test Hugo Project",
293 |         outputPath: configOutputDir,
294 |       });
295 | 
296 |       const hugoConfig = path.join(configOutputDir, "hugo.toml");
297 |       expect(
298 |         await fs
299 |           .access(hugoConfig)
300 |           .then(() => true)
301 |           .catch(() => false),
302 |       ).toBe(true);
303 | 
304 |       const configContent = await fs.readFile(hugoConfig, "utf-8");
305 |       expect(configContent).toContain("Test Hugo Project");
306 |     });
307 | 
308 |     it("should generate Jekyll configuration", async () => {
309 |       const result = await generateConfig({
310 |         ssg: "jekyll",
311 |         projectName: "Test Jekyll Project",
312 |         outputPath: configOutputDir,
313 |       });
314 | 
315 |       const jekyllConfig = path.join(configOutputDir, "_config.yml");
316 |       const gemfile = path.join(configOutputDir, "Gemfile");
317 | 
318 |       expect(
319 |         await fs
320 |           .access(jekyllConfig)
321 |           .then(() => true)
322 |           .catch(() => false),
323 |       ).toBe(true);
324 |       expect(
325 |         await fs
326 |           .access(gemfile)
327 |           .then(() => true)
328 |           .catch(() => false),
329 |       ).toBe(true);
330 |     });
331 | 
332 |     it("should generate Eleventy configuration", async () => {
333 |       const result = await generateConfig({
334 |         ssg: "eleventy",
335 |         projectName: "Test Eleventy Project",
336 |         outputPath: configOutputDir,
337 |       });
338 | 
339 |       const eleventyConfig = path.join(configOutputDir, ".eleventy.js");
340 |       const packageJson = path.join(configOutputDir, "package.json");
341 | 
342 |       expect(
343 |         await fs
344 |           .access(eleventyConfig)
345 |           .then(() => true)
346 |           .catch(() => false),
347 |       ).toBe(true);
348 |       expect(
349 |         await fs
350 |           .access(packageJson)
351 |           .then(() => true)
352 |           .catch(() => false),
353 |       ).toBe(true);
354 |     });
355 |   });
356 | 
357 |   describe("setup_structure Tool", () => {
358 |     let structureOutputDir: string;
359 | 
360 |     beforeEach(async () => {
361 |       structureOutputDir = path.join(
362 |         tempDir,
363 |         "structure-output",
364 |         Date.now().toString(),
365 |       );
366 |     });
367 | 
368 |     it("should create Diataxis structure with examples", async () => {
369 |       const result = await setupStructure({
370 |         path: structureOutputDir,
371 |         ssg: "docusaurus",
372 |         includeExamples: true,
373 |       });
374 | 
375 |       expect(result.content).toBeDefined();
376 | 
377 |       // Verify directory structure
378 |       const categories = ["tutorials", "how-to", "reference", "explanation"];
379 |       for (const category of categories) {
380 |         const categoryDir = path.join(structureOutputDir, category);
381 |         expect(
382 |           await fs
383 |             .access(categoryDir)
384 |             .then(() => true)
385 |             .catch(() => false),
386 |         ).toBe(true);
387 | 
388 |         // Check for index.md
389 |         const indexFile = path.join(categoryDir, "index.md");
390 |         expect(
391 |           await fs
392 |             .access(indexFile)
393 |             .then(() => true)
394 |             .catch(() => false),
395 |         ).toBe(true);
396 | 
397 |         // Check for example file
398 |         const files = await fs.readdir(categoryDir);
399 |         expect(files.length).toBeGreaterThan(1); // index.md + example file
400 |       }
401 | 
402 |       // Check root index
403 |       const rootIndex = path.join(structureOutputDir, "index.md");
404 |       expect(
405 |         await fs
406 |           .access(rootIndex)
407 |           .then(() => true)
408 |           .catch(() => false),
409 |       ).toBe(true);
410 | 
411 |       const rootContent = await fs.readFile(rootIndex, "utf-8");
412 |       expect(rootContent).toContain("Diataxis");
413 |       expect(rootContent).toContain("Tutorials");
414 |       expect(rootContent).toContain("How-To Guides");
415 |     });
416 | 
417 |     it("should create structure without examples", async () => {
418 |       const result = await setupStructure({
419 |         path: structureOutputDir,
420 |         ssg: "mkdocs",
421 |         includeExamples: false,
422 |       });
423 | 
424 |       expect(result.content).toBeDefined();
425 | 
426 |       // Verify only index files exist (no examples)
427 |       const tutorialsDir = path.join(structureOutputDir, "tutorials");
428 |       const files = await fs.readdir(tutorialsDir);
429 |       expect(files).toEqual(["index.md"]); // Only index, no example
430 |     });
431 | 
432 |     it("should handle different SSG formats correctly", async () => {
433 |       // Test Docusaurus format
434 |       await setupStructure({
435 |         path: path.join(structureOutputDir, "docusaurus"),
436 |         ssg: "docusaurus",
437 |         includeExamples: true,
438 |       });
439 | 
440 |       const docusaurusFile = path.join(
441 |         structureOutputDir,
442 |         "docusaurus",
443 |         "tutorials",
444 |         "index.md",
445 |       );
446 |       const docusaurusContent = await fs.readFile(docusaurusFile, "utf-8");
447 |       expect(docusaurusContent).toContain("id: tutorials-index");
448 |       expect(docusaurusContent).toContain("sidebar_label:");
449 | 
450 |       // Test Jekyll format
451 |       await setupStructure({
452 |         path: path.join(structureOutputDir, "jekyll"),
453 |         ssg: "jekyll",
454 |         includeExamples: true,
455 |       });
456 | 
457 |       const jekyllFile = path.join(
458 |         structureOutputDir,
459 |         "jekyll",
460 |         "tutorials",
461 |         "index.md",
462 |       );
463 |       const jekyllContent = await fs.readFile(jekyllFile, "utf-8");
464 |       expect(jekyllContent).toContain("title:");
465 |       expect(jekyllContent).toContain("description:");
466 |     });
467 |   });
468 | 
469 |   describe("deploy_pages Tool", () => {
470 |     let deploymentRepoDir: string;
471 | 
472 |     beforeEach(async () => {
473 |       deploymentRepoDir = path.join(
474 |         tempDir,
475 |         "deployment-repo",
476 |         Date.now().toString(),
477 |       );
478 |       await fs.mkdir(deploymentRepoDir, { recursive: true });
479 |     });
480 | 
481 |     it("should create GitHub Actions workflow for Docusaurus", async () => {
482 |       const result = await deployPages({
483 |         repository: deploymentRepoDir,
484 |         ssg: "docusaurus",
485 |         branch: "gh-pages",
486 |       });
487 | 
488 |       expect(result.content).toBeDefined();
489 | 
490 |       const workflowPath = path.join(
491 |         deploymentRepoDir,
492 |         ".github",
493 |         "workflows",
494 |         "deploy-docs.yml",
495 |       );
496 |       expect(
497 |         await fs
498 |           .access(workflowPath)
499 |           .then(() => true)
500 |           .catch(() => false),
501 |       ).toBe(true);
502 | 
503 |       const workflowContent = await fs.readFile(workflowPath, "utf-8");
504 |       expect(workflowContent).toContain("Deploy Docusaurus");
505 |       expect(workflowContent).toContain("npm run build");
506 |       expect(workflowContent).toContain("actions/upload-pages-artifact");
507 |       expect(workflowContent).toContain("actions/deploy-pages");
508 | 
509 |       // Verify security compliance (OIDC tokens)
510 |       expect(workflowContent).toContain("id-token: write");
511 |       expect(workflowContent).toContain("pages: write");
512 |       expect(workflowContent).not.toContain("GITHUB_TOKEN: ${{ secrets.");
513 |     });
514 | 
515 |     it("should create workflow for MkDocs", async () => {
516 |       const result = await deployPages({
517 |         repository: deploymentRepoDir,
518 |         ssg: "mkdocs",
519 |       });
520 | 
521 |       const workflowPath = path.join(
522 |         deploymentRepoDir,
523 |         ".github",
524 |         "workflows",
525 |         "deploy-docs.yml",
526 |       );
527 |       const workflowContent = await fs.readFile(workflowPath, "utf-8");
528 | 
529 |       expect(workflowContent).toContain("Deploy MkDocs");
530 |       expect(workflowContent).toContain("mkdocs gh-deploy");
531 |       expect(workflowContent).toContain("python");
532 |     });
533 | 
534 |     it("should create workflow for Hugo", async () => {
535 |       const result = await deployPages({
536 |         repository: deploymentRepoDir,
537 |         ssg: "hugo",
538 |       });
539 | 
540 |       const workflowContent = await fs.readFile(
541 |         path.join(deploymentRepoDir, ".github", "workflows", "deploy-docs.yml"),
542 |         "utf-8",
543 |       );
544 | 
545 |       expect(workflowContent).toContain("Deploy Hugo");
546 |       expect(workflowContent).toContain("peaceiris/actions-hugo");
547 |       expect(workflowContent).toContain("hugo --minify");
548 |     });
549 | 
550 |     it("should handle custom domain configuration", async () => {
551 |       const result = await deployPages({
552 |         repository: deploymentRepoDir,
553 |         ssg: "jekyll",
554 |         customDomain: "docs.example.com",
555 |       });
556 | 
557 |       // Check CNAME file creation
558 |       const cnamePath = path.join(deploymentRepoDir, "CNAME");
559 |       expect(
560 |         await fs
561 |           .access(cnamePath)
562 |           .then(() => true)
563 |           .catch(() => false),
564 |       ).toBe(true);
565 | 
566 |       const cnameContent = await fs.readFile(cnamePath, "utf-8");
567 |       expect(cnameContent.trim()).toBe("docs.example.com");
568 | 
569 |       // Verify result indicates custom domain was configured
570 |       const resultText = result.content.map((c) => c.text).join(" ");
571 |       expect(resultText).toContain("docs.example.com");
572 |     });
573 |   });
574 | 
575 |   describe("verify_deployment Tool", () => {
576 |     let verificationRepoDir: string;
577 | 
578 |     beforeEach(async () => {
579 |       verificationRepoDir = path.join(
580 |         tempDir,
581 |         "verification-repo",
582 |         Date.now().toString(),
583 |       );
584 |       await fs.mkdir(verificationRepoDir, { recursive: true });
585 |     });
586 | 
587 |     it("should verify complete deployment setup", async () => {
588 |       // Set up a complete deployment scenario
589 |       await fs.mkdir(path.join(verificationRepoDir, ".github", "workflows"), {
590 |         recursive: true,
591 |       });
592 |       await fs.mkdir(path.join(verificationRepoDir, "docs"), {
593 |         recursive: true,
594 |       });
595 |       await fs.mkdir(path.join(verificationRepoDir, "build"), {
596 |         recursive: true,
597 |       });
598 | 
599 |       // Create workflow file
600 |       await fs.writeFile(
601 |         path.join(
602 |           verificationRepoDir,
603 |           ".github",
604 |           "workflows",
605 |           "deploy-docs.yml",
606 |         ),
607 |         "name: Deploy Docs\non: push\njobs:\n  deploy:\n    runs-on: ubuntu-latest",
608 |       );
609 | 
610 |       // Create documentation files
611 |       await fs.writeFile(
612 |         path.join(verificationRepoDir, "docs", "index.md"),
613 |         "# Documentation",
614 |       );
615 |       await fs.writeFile(
616 |         path.join(verificationRepoDir, "docs", "guide.md"),
617 |         "# Guide",
618 |       );
619 | 
620 |       // Create config file
621 |       await fs.writeFile(
622 |         path.join(verificationRepoDir, "docusaurus.config.js"),
623 |         'module.exports = { title: "Test" };',
624 |       );
625 | 
626 |       // Create build directory
627 |       await fs.writeFile(
628 |         path.join(verificationRepoDir, "build", "index.html"),
629 |         "<h1>Built Site</h1>",
630 |       );
631 | 
632 |       const result = await verifyDeployment({
633 |         repository: verificationRepoDir,
634 |         url: "https://example.github.io/test-repo",
635 |       });
636 | 
637 |       expect(result.content).toBeDefined();
638 | 
639 |       // Parse the verification result
640 |       const verification = JSON.parse(result.content[0].text);
641 |       expect(verification.summary.passed).toBeGreaterThan(0); // Should have passing checks
642 |       expect(
643 |         verification.checks.some((check: any) =>
644 |           check.message.includes("deployment workflow"),
645 |         ),
646 |       ).toBe(true);
647 |       expect(
648 |         verification.checks.some((check: any) =>
649 |           check.message.includes("documentation files"),
650 |         ),
651 |       ).toBe(true);
652 |       expect(
653 |         verification.checks.some((check: any) =>
654 |           check.message.includes("configuration"),
655 |         ),
656 |       ).toBe(true);
657 |       expect(
658 |         verification.checks.some((check: any) =>
659 |           check.message.includes("build output"),
660 |         ),
661 |       ).toBe(true);
662 |     });
663 | 
664 |     it("should identify missing components", async () => {
665 |       // Create minimal repo without deployment setup
666 |       await fs.writeFile(
667 |         path.join(verificationRepoDir, "README.md"),
668 |         "# Test Repo",
669 |       );
670 | 
671 |       const result = await verifyDeployment({
672 |         repository: verificationRepoDir,
673 |       });
674 | 
675 |       const verification = JSON.parse(result.content[0].text);
676 |       expect(verification.summary.failed).toBeGreaterThan(0); // Should have failing checks
677 |       expect(
678 |         verification.checks.some((check: any) =>
679 |           check.message.includes("No .github/workflows"),
680 |         ),
681 |       ).toBe(true);
682 |       expect(
683 |         verification.checks.some((check: any) =>
684 |           check.message.includes("No documentation files"),
685 |         ),
686 |       ).toBe(true);
687 |       expect(
688 |         verification.checks.some((check: any) =>
689 |           check.message.includes("No static site generator configuration"),
690 |         ),
691 |       ).toBe(true);
692 |     });
693 | 
694 |     it("should provide actionable recommendations", async () => {
695 |       const result = await verifyDeployment({
696 |         repository: verificationRepoDir,
697 |       });
698 | 
699 |       const resultText = result.content.map((c) => c.text).join("\n");
700 |       expect(resultText).toContain("→"); // Should contain recommendation arrows
701 |       expect(resultText).toContain("deploy_pages tool");
702 |       expect(resultText).toContain("setup_structure tool");
703 |       expect(resultText).toContain("generate_config tool");
704 |     });
705 | 
706 |     it("should handle repository path variations", async () => {
707 |       // Test with relative path
708 |       const relativeResult = await verifyDeployment({
709 |         repository: ".",
710 |       });
711 |       expect(relativeResult.content).toBeDefined();
712 | 
713 |       // Test with absolute path
714 |       const absoluteResult = await verifyDeployment({
715 |         repository: verificationRepoDir,
716 |       });
717 |       expect(absoluteResult.content).toBeDefined();
718 | 
719 |       // Test with HTTP URL (should default to current directory)
720 |       const urlResult = await verifyDeployment({
721 |         repository: "https://github.com/user/repo",
722 |       });
723 |       expect(urlResult.content).toBeDefined();
724 |     });
725 |   });
726 | 
727 |   // Helper functions to create test repositories
728 |   async function createJavaScriptRepo(): Promise<string> {
729 |     const repoPath = path.join(tempDir, "javascript-repo");
730 |     await fs.mkdir(repoPath, { recursive: true });
731 | 
732 |     // package.json
733 |     const packageJson = {
734 |       name: "test-js-project",
735 |       version: "1.0.0",
736 |       description: "Test JavaScript project",
737 |       scripts: {
738 |         start: "node index.js",
739 |         test: "jest",
740 |       },
741 |       dependencies: {
742 |         express: "^4.18.0",
743 |         lodash: "^4.17.21",
744 |       },
745 |       devDependencies: {
746 |         jest: "^29.0.0",
747 |         "@types/node": "^20.0.0",
748 |       },
749 |     };
750 |     await fs.writeFile(
751 |       path.join(repoPath, "package.json"),
752 |       JSON.stringify(packageJson, null, 2),
753 |     );
754 | 
755 |     // Source files
756 |     await fs.writeFile(
757 |       path.join(repoPath, "index.js"),
758 |       'console.log("Hello World");',
759 |     );
760 |     await fs.writeFile(
761 |       path.join(repoPath, "utils.js"),
762 |       "module.exports = { helper: () => {} };",
763 |     );
764 |     await fs.writeFile(
765 |       path.join(repoPath, "app.ts"),
766 |       'const app: string = "TypeScript";',
767 |     );
768 | 
769 |     // Test directory
770 |     await fs.mkdir(path.join(repoPath, "test"), { recursive: true });
771 |     await fs.writeFile(
772 |       path.join(repoPath, "test", "app.test.js"),
773 |       'test("example", () => {});',
774 |     );
775 | 
776 |     // Documentation
777 |     await fs.writeFile(
778 |       path.join(repoPath, "README.md"),
779 |       "# JavaScript Test Project\nA test project for JavaScript analysis.",
780 |     );
781 |     await fs.writeFile(
782 |       path.join(repoPath, "CONTRIBUTING.md"),
783 |       "# Contributing\nHow to contribute.",
784 |     );
785 |     await fs.writeFile(path.join(repoPath, "LICENSE"), "MIT License");
786 | 
787 |     // CI/CD
788 |     await fs.mkdir(path.join(repoPath, ".github", "workflows"), {
789 |       recursive: true,
790 |     });
791 |     await fs.writeFile(
792 |       path.join(repoPath, ".github", "workflows", "ci.yml"),
793 |       "name: CI\non: push\njobs:\n  test:\n    runs-on: ubuntu-latest",
794 |     );
795 | 
796 |     return repoPath;
797 |   }
798 | 
799 |   async function createPythonRepo(): Promise<string> {
800 |     const repoPath = path.join(tempDir, "python-repo");
801 |     await fs.mkdir(repoPath, { recursive: true });
802 | 
803 |     // requirements.txt
804 |     await fs.writeFile(
805 |       path.join(repoPath, "requirements.txt"),
806 |       "flask>=2.0.0\nrequests>=2.25.0\nnumpy>=1.21.0",
807 |     );
808 | 
809 |     // Python files
810 |     await fs.writeFile(
811 |       path.join(repoPath, "main.py"),
812 |       "import flask\napp = flask.Flask(__name__)",
813 |     );
814 |     await fs.writeFile(
815 |       path.join(repoPath, "utils.py"),
816 |       "def helper():\n    pass",
817 |     );
818 | 
819 |     // Tests
820 |     await fs.mkdir(path.join(repoPath, "tests"), { recursive: true });
821 |     await fs.writeFile(
822 |       path.join(repoPath, "tests", "test_main.py"),
823 |       "def test_app():\n    assert True",
824 |     );
825 | 
826 |     await fs.writeFile(
827 |       path.join(repoPath, "README.md"),
828 |       "# Python Test Project",
829 |     );
830 | 
831 |     return repoPath;
832 |   }
833 | 
834 |   async function createRubyRepo(): Promise<string> {
835 |     const repoPath = path.join(tempDir, "ruby-repo");
836 |     await fs.mkdir(repoPath, { recursive: true });
837 | 
838 |     // Gemfile
839 |     await fs.writeFile(
840 |       path.join(repoPath, "Gemfile"),
841 |       'source "https://rubygems.org"\ngem "rails"',
842 |     );
843 | 
844 |     // Ruby files
845 |     await fs.writeFile(path.join(repoPath, "app.rb"), "class App\nend");
846 |     await fs.writeFile(path.join(repoPath, "helper.rb"), "module Helper\nend");
847 | 
848 |     await fs.writeFile(path.join(repoPath, "README.md"), "# Ruby Test Project");
849 | 
850 |     return repoPath;
851 |   }
852 | 
853 |   async function createGoRepo(): Promise<string> {
854 |     const repoPath = path.join(tempDir, "go-repo");
855 |     await fs.mkdir(repoPath, { recursive: true });
856 | 
857 |     // go.mod
858 |     await fs.writeFile(
859 |       path.join(repoPath, "go.mod"),
860 |       "module test-go-project\ngo 1.19",
861 |     );
862 | 
863 |     // Go files
864 |     await fs.writeFile(
865 |       path.join(repoPath, "main.go"),
866 |       "package main\nfunc main() {}",
867 |     );
868 |     await fs.writeFile(
869 |       path.join(repoPath, "utils.go"),
870 |       "package main\nfunc helper() {}",
871 |     );
872 | 
873 |     await fs.writeFile(path.join(repoPath, "README.md"), "# Go Test Project");
874 | 
875 |     return repoPath;
876 |   }
877 | 
878 |   async function createMixedLanguageRepo(): Promise<string> {
879 |     const repoPath = path.join(tempDir, "mixed-repo");
880 |     await fs.mkdir(repoPath, { recursive: true });
881 | 
882 |     // Multiple language files
883 |     await fs.writeFile(
884 |       path.join(repoPath, "package.json"),
885 |       '{"name": "mixed-project"}',
886 |     );
887 |     await fs.writeFile(path.join(repoPath, "requirements.txt"), "flask>=2.0.0");
888 |     await fs.writeFile(path.join(repoPath, "Gemfile"), 'gem "rails"');
889 | 
890 |     await fs.writeFile(path.join(repoPath, "app.js"), 'console.log("JS");');
891 |     await fs.writeFile(path.join(repoPath, "script.py"), 'print("Python")');
892 |     await fs.writeFile(path.join(repoPath, "server.rb"), 'puts "Ruby"');
893 | 
894 |     await fs.writeFile(
895 |       path.join(repoPath, "README.md"),
896 |       "# Mixed Language Project",
897 |     );
898 | 
899 |     return repoPath;
900 |   }
901 | 
902 |   async function createLargeRepo(): Promise<string> {
903 |     const repoPath = path.join(tempDir, "large-repo");
904 |     await fs.mkdir(repoPath, { recursive: true });
905 | 
906 |     // Create many files to simulate a large repository
907 |     for (let i = 0; i < 150; i++) {
908 |       const fileName = `file-${i.toString().padStart(3, "0")}.js`;
909 |       await fs.writeFile(
910 |         path.join(repoPath, fileName),
911 |         `// File ${i}\nconsole.log(${i});`,
912 |       );
913 |     }
914 | 
915 |     // Create nested directories
916 |     for (let i = 0; i < 10; i++) {
917 |       const dirPath = path.join(repoPath, `dir-${i}`);
918 |       await fs.mkdir(dirPath, { recursive: true });
919 | 
920 |       for (let j = 0; j < 20; j++) {
921 |         const fileName = `nested-${j}.js`;
922 |         await fs.writeFile(
923 |           path.join(dirPath, fileName),
924 |           `// Nested file ${i}-${j}`,
925 |         );
926 |       }
927 |     }
928 | 
929 |     await fs.writeFile(
930 |       path.join(repoPath, "package.json"),
931 |       '{"name": "large-project"}',
932 |     );
933 |     await fs.writeFile(
934 |       path.join(repoPath, "README.md"),
935 |       "# Large Test Project",
936 |     );
937 | 
938 |     return repoPath;
939 |   }
940 | 
941 |   async function createEmptyRepo(): Promise<string> {
942 |     const repoPath = path.join(tempDir, "empty-repo");
943 |     await fs.mkdir(repoPath, { recursive: true });
944 | 
945 |     // Only a README file
946 |     await fs.writeFile(
947 |       path.join(repoPath, "README.md"),
948 |       "# Empty Project\nMinimal repository for testing.",
949 |     );
950 | 
951 |     return repoPath;
952 |   }
953 | });
954 | 
```
Page 19/29FirstPrevNextLast