#
tokens: 46089/50000 9/274 files (page 9/29)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 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/memory/kg-link-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { promises as fs } from "fs";
  2 | import path from "path";
  3 | import os from "os";
  4 | import {
  5 |   validateExternalLinks,
  6 |   validateAndStoreDocumentationLinks,
  7 |   extractLinksFromContent,
  8 |   storeLinkValidationInKG,
  9 |   getLinkValidationHistory,
 10 | } from "../../src/memory/kg-link-validator";
 11 | import { getKnowledgeGraph } from "../../src/memory/kg-integration";
 12 | 
 13 | describe("KG Link Validator", () => {
 14 |   let tempDir: string;
 15 |   const originalCwd = process.cwd();
 16 | 
 17 |   beforeEach(async () => {
 18 |     tempDir = path.join(os.tmpdir(), `kg-link-${Date.now()}`);
 19 |     await fs.mkdir(tempDir, { recursive: true });
 20 |     process.chdir(tempDir);
 21 |   });
 22 | 
 23 |   afterEach(async () => {
 24 |     process.chdir(originalCwd);
 25 |     try {
 26 |       await fs.rm(tempDir, { recursive: true, force: true });
 27 |     } catch {
 28 |       // Ignore cleanup errors
 29 |     }
 30 |   });
 31 | 
 32 |   describe("validateExternalLinks", () => {
 33 |     it("should validate valid URLs", async () => {
 34 |       const urls = ["https://www.google.com", "https://github.com"];
 35 | 
 36 |       const result = await validateExternalLinks(urls, {
 37 |         timeout: 5000,
 38 |       });
 39 | 
 40 |       expect(result.totalLinks).toBe(2);
 41 |       expect(result.results).toHaveLength(2);
 42 |       expect(result.validLinks + result.brokenLinks + result.unknownLinks).toBe(
 43 |         2,
 44 |       );
 45 |     });
 46 | 
 47 |     it("should detect broken links", async () => {
 48 |       const urls = ["https://this-domain-definitely-does-not-exist-12345.com"];
 49 | 
 50 |       const result = await validateExternalLinks(urls, {
 51 |         timeout: 3000,
 52 |       });
 53 | 
 54 |       expect(result.totalLinks).toBe(1);
 55 |       expect(result.results).toHaveLength(1);
 56 |       // Should be either broken or unknown
 57 |       expect(result.brokenLinks + result.unknownLinks).toBeGreaterThan(0);
 58 |     });
 59 | 
 60 |     it("should handle empty URL list", async () => {
 61 |       const result = await validateExternalLinks([]);
 62 | 
 63 |       expect(result.totalLinks).toBe(0);
 64 |       expect(result.results).toHaveLength(0);
 65 |     });
 66 | 
 67 |     it("should respect timeout option", async () => {
 68 |       const urls = ["https://www.google.com"];
 69 | 
 70 |       const startTime = Date.now();
 71 |       await validateExternalLinks(urls, {
 72 |         timeout: 1000,
 73 |       });
 74 |       const duration = Date.now() - startTime;
 75 | 
 76 |       // Should complete reasonably quickly
 77 |       expect(duration).toBeLessThan(10000);
 78 |     });
 79 | 
 80 |     it("should use default timeout when not provided", async () => {
 81 |       const urls = ["https://www.google.com"];
 82 | 
 83 |       const result = await validateExternalLinks(urls);
 84 | 
 85 |       expect(result).toBeDefined();
 86 |       expect(result.totalLinks).toBe(1);
 87 |     });
 88 | 
 89 |     it("should handle validation errors gracefully", async () => {
 90 |       const urls = ["https://httpstat.us/500"]; // Returns 500 error
 91 | 
 92 |       const result = await validateExternalLinks(urls, {
 93 |         timeout: 5000,
 94 |       });
 95 | 
 96 |       expect(result.totalLinks).toBe(1);
 97 |       // Should be marked as broken or unknown
 98 |       expect(result.brokenLinks + result.unknownLinks).toBeGreaterThan(0);
 99 |     });
100 | 
101 |     it("should count warning links correctly", async () => {
102 |       const urls = ["https://httpstat.us/301"]; // Redirect
103 | 
104 |       const result = await validateExternalLinks(urls, {
105 |         timeout: 5000,
106 |       });
107 | 
108 |       expect(result.totalLinks).toBe(1);
109 |       // Should handle redirects as valid (fetch follows redirects)
110 |       expect(
111 |         result.validLinks +
112 |           result.brokenLinks +
113 |           result.warningLinks +
114 |           result.unknownLinks,
115 |       ).toBe(1);
116 |     });
117 | 
118 |     it("should handle network errors in validation loop", async () => {
119 |       const urls = ["https://invalid-url-12345.test", "https://www.google.com"];
120 | 
121 |       const result = await validateExternalLinks(urls, {
122 |         timeout: 3000,
123 |       });
124 | 
125 |       expect(result.totalLinks).toBe(2);
126 |       expect(result.results).toHaveLength(2);
127 |     });
128 | 
129 |     it("should include response time in valid results", async () => {
130 |       const urls = ["https://www.google.com"];
131 | 
132 |       const result = await validateExternalLinks(urls, {
133 |         timeout: 5000,
134 |       });
135 | 
136 |       expect(result.results[0].lastChecked).toBeDefined();
137 |       if (result.results[0].status === "valid") {
138 |         expect(result.results[0].responseTime).toBeDefined();
139 |         expect(result.results[0].responseTime).toBeGreaterThan(0);
140 |       }
141 |     });
142 | 
143 |     it("should include response time in broken results", async () => {
144 |       const urls = ["https://httpstat.us/404"];
145 | 
146 |       const result = await validateExternalLinks(urls, {
147 |         timeout: 5000,
148 |       });
149 | 
150 |       expect(result.results[0].lastChecked).toBeDefined();
151 |       if (
152 |         result.results[0].status === "broken" &&
153 |         result.results[0].statusCode
154 |       ) {
155 |         expect(result.results[0].responseTime).toBeDefined();
156 |       }
157 |     });
158 |   });
159 | 
160 |   describe("extractLinksFromContent", () => {
161 |     it("should extract external links", () => {
162 |       const content = `
163 |         # Test
164 |         [Google](https://www.google.com)
165 |         [GitHub](https://github.com)
166 |       `;
167 | 
168 |       const result = extractLinksFromContent(content);
169 | 
170 |       expect(result.externalLinks.length).toBeGreaterThan(0);
171 |     });
172 | 
173 |     it("should extract internal links", () => {
174 |       const content = `
175 |         # Test
176 |         [Page 1](./page1.md)
177 |         [Page 2](../page2.md)
178 |       `;
179 | 
180 |       const result = extractLinksFromContent(content);
181 | 
182 |       expect(result.internalLinks.length).toBeGreaterThan(0);
183 |     });
184 | 
185 |     it("should handle mixed links", () => {
186 |       const content = `
187 |         # Test
188 |         [External](https://example.com)
189 |         [Internal](./page.md)
190 |       `;
191 | 
192 |       const result = extractLinksFromContent(content);
193 | 
194 |       expect(result.externalLinks.length).toBeGreaterThan(0);
195 |       expect(result.internalLinks.length).toBeGreaterThan(0);
196 |     });
197 | 
198 |     it("should extract HTTP links", () => {
199 |       const content = `[Link](http://example.com)`;
200 | 
201 |       const result = extractLinksFromContent(content);
202 | 
203 |       expect(result.externalLinks).toContain("http://example.com");
204 |     });
205 | 
206 |     it("should extract HTML anchor links", () => {
207 |       const content = `<a href="https://example.com">Link</a>`;
208 | 
209 |       const result = extractLinksFromContent(content);
210 | 
211 |       expect(result.externalLinks).toContain("https://example.com");
212 |     });
213 | 
214 |     it("should extract HTML anchor links with single quotes", () => {
215 |       const content = `<a href='https://example.com'>Link</a>`;
216 | 
217 |       const result = extractLinksFromContent(content);
218 | 
219 |       expect(result.externalLinks).toContain("https://example.com");
220 |     });
221 | 
222 |     it("should extract internal HTML links", () => {
223 |       const content = `<a href="./page.md">Link</a>`;
224 | 
225 |       const result = extractLinksFromContent(content);
226 | 
227 |       expect(result.internalLinks).toContain("./page.md");
228 |     });
229 | 
230 |     it("should remove duplicate links", () => {
231 |       const content = `
232 |         [Link1](https://example.com)
233 |         [Link2](https://example.com)
234 |         [Link3](./page.md)
235 |         [Link4](./page.md)
236 |       `;
237 | 
238 |       const result = extractLinksFromContent(content);
239 | 
240 |       expect(result.externalLinks.length).toBe(1);
241 |       expect(result.internalLinks.length).toBe(1);
242 |     });
243 | 
244 |     it("should handle content with no links", () => {
245 |       const content = "# Test\nNo links here";
246 | 
247 |       const result = extractLinksFromContent(content);
248 | 
249 |       expect(result.externalLinks).toEqual([]);
250 |       expect(result.internalLinks).toEqual([]);
251 |     });
252 |   });
253 | 
254 |   describe("validateAndStoreDocumentationLinks", () => {
255 |     it("should validate and store documentation links", async () => {
256 |       const content =
257 |         "# Test\n[Link](./other.md)\n[External](https://example.com)";
258 | 
259 |       const result = await validateAndStoreDocumentationLinks(
260 |         "test-project",
261 |         content,
262 |       );
263 | 
264 |       expect(result).toBeDefined();
265 |       expect(result.totalLinks).toBeGreaterThan(0);
266 |     });
267 | 
268 |     it("should handle documentation without links", async () => {
269 |       const content = "# Test\nNo links here";
270 | 
271 |       const result = await validateAndStoreDocumentationLinks(
272 |         "test-project",
273 |         content,
274 |       );
275 | 
276 |       expect(result).toBeDefined();
277 |       expect(result.totalLinks).toBe(0);
278 |     });
279 | 
280 |     it("should handle content with only internal links", async () => {
281 |       const content = "# Test\n[Page](./page.md)";
282 | 
283 |       const result = await validateAndStoreDocumentationLinks(
284 |         "test-project",
285 |         content,
286 |       );
287 | 
288 |       expect(result).toBeDefined();
289 |       // Only external links are validated
290 |       expect(result.totalLinks).toBe(0);
291 |     });
292 |   });
293 | 
294 |   describe("storeLinkValidationInKG", () => {
295 |     it("should store validation results with no broken links", async () => {
296 |       const summary = {
297 |         totalLinks: 5,
298 |         validLinks: 5,
299 |         brokenLinks: 0,
300 |         warningLinks: 0,
301 |         unknownLinks: 0,
302 |         results: [],
303 |       };
304 | 
305 |       await storeLinkValidationInKG("doc-section-1", summary);
306 | 
307 |       const kg = await getKnowledgeGraph();
308 |       const nodes = await kg.getAllNodes();
309 | 
310 |       // Find the specific validation node for this test
311 |       const validationNode = nodes.find(
312 |         (n) =>
313 |           n.type === "link_validation" &&
314 |           n.properties.totalLinks === 5 &&
315 |           n.properties.brokenLinks === 0,
316 |       );
317 |       expect(validationNode).toBeDefined();
318 |       expect(validationNode?.properties.totalLinks).toBe(5);
319 |       expect(validationNode?.properties.healthScore).toBe(100);
320 |     });
321 | 
322 |     it("should store validation results with broken links", async () => {
323 |       const summary = {
324 |         totalLinks: 10,
325 |         validLinks: 7,
326 |         brokenLinks: 3,
327 |         warningLinks: 0,
328 |         unknownLinks: 0,
329 |         results: [
330 |           {
331 |             url: "https://broken1.com",
332 |             status: "broken" as const,
333 |             lastChecked: new Date().toISOString(),
334 |           },
335 |           {
336 |             url: "https://broken2.com",
337 |             status: "broken" as const,
338 |             lastChecked: new Date().toISOString(),
339 |           },
340 |           {
341 |             url: "https://broken3.com",
342 |             status: "broken" as const,
343 |             lastChecked: new Date().toISOString(),
344 |           },
345 |         ],
346 |       };
347 | 
348 |       await storeLinkValidationInKG("doc-section-2", summary);
349 | 
350 |       const kg = await getKnowledgeGraph();
351 |       const edges = await kg.findEdges({
352 |         source: "doc-section-2",
353 |         type: "has_link_validation",
354 |       });
355 | 
356 |       expect(edges.length).toBeGreaterThan(0);
357 |     });
358 | 
359 |     it("should create requires_fix edge for broken links", async () => {
360 |       const summary = {
361 |         totalLinks: 10,
362 |         validLinks: 4,
363 |         brokenLinks: 6,
364 |         warningLinks: 0,
365 |         unknownLinks: 0,
366 |         results: [
367 |           {
368 |             url: "https://broken.com",
369 |             status: "broken" as const,
370 |             lastChecked: new Date().toISOString(),
371 |           },
372 |         ],
373 |       };
374 | 
375 |       await storeLinkValidationInKG("doc-section-3", summary);
376 | 
377 |       const kg = await getKnowledgeGraph();
378 |       const allNodes = await kg.getAllNodes();
379 |       const validationNode = allNodes.find(
380 |         (n) => n.type === "link_validation" && n.properties.brokenLinks === 6,
381 |       );
382 | 
383 |       expect(validationNode).toBeDefined();
384 | 
385 |       const requiresFixEdges = await kg.findEdges({
386 |         source: validationNode!.id,
387 |         type: "requires_fix",
388 |       });
389 | 
390 |       expect(requiresFixEdges.length).toBeGreaterThan(0);
391 |       expect(requiresFixEdges[0].properties.severity).toBe("high"); // > 5 broken links
392 |     });
393 | 
394 |     it("should set medium severity for few broken links", async () => {
395 |       const summary = {
396 |         totalLinks: 10,
397 |         validLinks: 8,
398 |         brokenLinks: 2,
399 |         warningLinks: 0,
400 |         unknownLinks: 0,
401 |         results: [
402 |           {
403 |             url: "https://broken.com",
404 |             status: "broken" as const,
405 |             lastChecked: new Date().toISOString(),
406 |           },
407 |         ],
408 |       };
409 | 
410 |       await storeLinkValidationInKG("doc-section-4", summary);
411 | 
412 |       const kg = await getKnowledgeGraph();
413 |       const allNodes = await kg.getAllNodes();
414 |       const validationNode = allNodes.find(
415 |         (n) => n.type === "link_validation" && n.properties.brokenLinks === 2,
416 |       );
417 | 
418 |       const requiresFixEdges = await kg.findEdges({
419 |         source: validationNode!.id,
420 |         type: "requires_fix",
421 |       });
422 | 
423 |       expect(requiresFixEdges[0].properties.severity).toBe("medium");
424 |     });
425 | 
426 |     it("should calculate health score correctly", async () => {
427 |       const summary = {
428 |         totalLinks: 20,
429 |         validLinks: 15,
430 |         brokenLinks: 5,
431 |         warningLinks: 0,
432 |         unknownLinks: 0,
433 |         results: [],
434 |       };
435 | 
436 |       await storeLinkValidationInKG("doc-section-5", summary);
437 | 
438 |       const kg = await getKnowledgeGraph();
439 |       const nodes = await kg.getAllNodes();
440 | 
441 |       const validationNode = nodes.find(
442 |         (n) => n.type === "link_validation" && n.properties.totalLinks === 20,
443 |       );
444 | 
445 |       expect(validationNode?.properties.healthScore).toBe(75);
446 |     });
447 | 
448 |     it("should handle zero links with 100% health score", async () => {
449 |       const summary = {
450 |         totalLinks: 0,
451 |         validLinks: 0,
452 |         brokenLinks: 0,
453 |         warningLinks: 0,
454 |         unknownLinks: 0,
455 |         results: [],
456 |       };
457 | 
458 |       await storeLinkValidationInKG("doc-section-6", summary);
459 | 
460 |       const kg = await getKnowledgeGraph();
461 |       const nodes = await kg.getAllNodes();
462 | 
463 |       const validationNode = nodes.find(
464 |         (n) => n.type === "link_validation" && n.properties.totalLinks === 0,
465 |       );
466 | 
467 |       expect(validationNode?.properties.healthScore).toBe(100);
468 |     });
469 |   });
470 | 
471 |   describe("getLinkValidationHistory", () => {
472 |     it("should retrieve validation history", async () => {
473 |       const summary1 = {
474 |         totalLinks: 5,
475 |         validLinks: 5,
476 |         brokenLinks: 0,
477 |         warningLinks: 0,
478 |         unknownLinks: 0,
479 |         results: [],
480 |       };
481 | 
482 |       await storeLinkValidationInKG("doc-section-7", summary1);
483 | 
484 |       const history = await getLinkValidationHistory("doc-section-7");
485 | 
486 |       expect(history.length).toBeGreaterThan(0);
487 |       expect(history[0].type).toBe("link_validation");
488 |     });
489 | 
490 |     it("should return empty array for non-existent doc section", async () => {
491 |       const history = await getLinkValidationHistory("non-existent");
492 | 
493 |       expect(history).toEqual([]);
494 |     });
495 | 
496 |     it("should sort history by newest first", async () => {
497 |       // Add two validations with delay to ensure different timestamps
498 |       const summary1 = {
499 |         totalLinks: 5,
500 |         validLinks: 5,
501 |         brokenLinks: 0,
502 |         warningLinks: 0,
503 |         unknownLinks: 0,
504 |         results: [],
505 |       };
506 | 
507 |       await storeLinkValidationInKG("doc-section-8", summary1);
508 | 
509 |       // Small delay to ensure different timestamp
510 |       await new Promise((resolve) => setTimeout(resolve, 10));
511 | 
512 |       const summary2 = {
513 |         totalLinks: 6,
514 |         validLinks: 6,
515 |         brokenLinks: 0,
516 |         warningLinks: 0,
517 |         unknownLinks: 0,
518 |         results: [],
519 |       };
520 | 
521 |       await storeLinkValidationInKG("doc-section-8", summary2);
522 | 
523 |       const history = await getLinkValidationHistory("doc-section-8");
524 | 
525 |       expect(history.length).toBeGreaterThan(1);
526 |       // First item should be newest
527 |       const firstTimestamp = new Date(
528 |         history[0].properties.lastValidated,
529 |       ).getTime();
530 |       const secondTimestamp = new Date(
531 |         history[1].properties.lastValidated,
532 |       ).getTime();
533 |       expect(firstTimestamp).toBeGreaterThanOrEqual(secondTimestamp);
534 |     });
535 |   });
536 | });
537 | 
```

--------------------------------------------------------------------------------
/tests/memory/manager-advanced.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for uncovered branches in Memory Manager
  3 |  * Covers: getRelated (lines 171-202), export (lines 381-398), import (lines 409-415)
  4 |  */
  5 | 
  6 | import { promises as fs } from "fs";
  7 | import path from "path";
  8 | import os from "os";
  9 | import { MemoryManager } from "../../src/memory/manager.js";
 10 | import { MemoryEntry } from "../../src/memory/storage.js";
 11 | 
 12 | describe("MemoryManager - Advanced Features Coverage", () => {
 13 |   let manager: MemoryManager;
 14 |   let tempDir: string;
 15 | 
 16 |   beforeEach(async () => {
 17 |     tempDir = path.join(
 18 |       os.tmpdir(),
 19 |       `manager-advanced-test-${Date.now()}-${Math.random()
 20 |         .toString(36)
 21 |         .substr(2, 9)}`,
 22 |     );
 23 |     await fs.mkdir(tempDir, { recursive: true });
 24 |     manager = new MemoryManager(tempDir);
 25 |     await manager.initialize();
 26 |   });
 27 | 
 28 |   afterEach(async () => {
 29 |     try {
 30 |       await manager.close();
 31 |       await fs.rm(tempDir, { recursive: true, force: true });
 32 |     } catch (error) {
 33 |       // Ignore cleanup errors
 34 |     }
 35 |   });
 36 | 
 37 |   describe("getRelated - Tag-based Relationships (lines 189-195)", () => {
 38 |     it("should find related memories by overlapping tags", async () => {
 39 |       // Create entries with overlapping tags
 40 |       const entry1 = await manager.remember(
 41 |         "analysis",
 42 |         { name: "Project A" },
 43 |         {
 44 |           projectId: "proj-001",
 45 |           tags: ["typescript", "react", "frontend"],
 46 |         },
 47 |       );
 48 | 
 49 |       await manager.remember(
 50 |         "analysis",
 51 |         { name: "Project B" },
 52 |         {
 53 |           projectId: "proj-002",
 54 |           tags: ["typescript", "vue", "frontend"],
 55 |         },
 56 |       );
 57 | 
 58 |       await manager.remember(
 59 |         "analysis",
 60 |         { name: "Project C" },
 61 |         {
 62 |           projectId: "proj-003",
 63 |           tags: ["python", "backend"],
 64 |         },
 65 |       );
 66 | 
 67 |       // Get related memories for entry1 (should find project B via overlapping tags)
 68 |       const related = await manager.getRelated(entry1, 10);
 69 | 
 70 |       expect(related.length).toBeGreaterThan(0);
 71 | 
 72 |       // Should include Project B (shares typescript and frontend tags)
 73 |       const relatedNames = related.map((r) => r.data.name);
 74 |       expect(relatedNames).toContain("Project B");
 75 | 
 76 |       // Should not include entry1 itself
 77 |       expect(relatedNames).not.toContain("Project A");
 78 |     });
 79 | 
 80 |     it("should find related memories by same type (lines 182-186)", async () => {
 81 |       const entry1 = await manager.remember(
 82 |         "recommendation",
 83 |         { ssg: "jekyll" },
 84 |         { projectId: "proj-001" },
 85 |       );
 86 | 
 87 |       await manager.remember(
 88 |         "recommendation",
 89 |         { ssg: "hugo" },
 90 |         { projectId: "proj-002" },
 91 |       );
 92 | 
 93 |       await manager.remember(
 94 |         "analysis",
 95 |         { type: "different" },
 96 |         { projectId: "proj-003" },
 97 |       );
 98 | 
 99 |       const related = await manager.getRelated(entry1, 10);
100 | 
101 |       // Should find the other recommendation, not the analysis
102 |       expect(related.length).toBeGreaterThan(0);
103 |       const types = related.map((r) => r.type);
104 |       expect(types).toContain("recommendation");
105 |     });
106 | 
107 |     it("should find related memories by same project (lines 174-179)", async () => {
108 |       manager.setContext({ projectId: "shared-project" });
109 | 
110 |       const entry1 = await manager.remember(
111 |         "analysis",
112 |         { step: "step1" },
113 |         { projectId: "shared-project" },
114 |       );
115 | 
116 |       await manager.remember(
117 |         "analysis",
118 |         { step: "step2" },
119 |         { projectId: "shared-project" },
120 |       );
121 | 
122 |       await manager.remember(
123 |         "analysis",
124 |         { step: "step3" },
125 |         { projectId: "different-project" },
126 |       );
127 | 
128 |       const related = await manager.getRelated(entry1, 10);
129 | 
130 |       // Should find step2 from same project
131 |       expect(related.length).toBeGreaterThan(0);
132 |       const projectIds = related.map((r) => r.metadata.projectId);
133 |       expect(projectIds).toContain("shared-project");
134 |     });
135 | 
136 |     it("should deduplicate and limit related memories (lines 198-202)", async () => {
137 |       const entry1 = await manager.remember(
138 |         "analysis",
139 |         { name: "Entry 1" },
140 |         {
141 |           projectId: "proj-001",
142 |           tags: ["tag1", "tag2"],
143 |         },
144 |       );
145 | 
146 |       // Create many related entries
147 |       for (let i = 0; i < 20; i++) {
148 |         await manager.remember(
149 |           "analysis",
150 |           { name: `Entry ${i + 2}` },
151 |           {
152 |             projectId: "proj-001",
153 |             tags: i < 10 ? ["tag1"] : ["tag2"],
154 |           },
155 |         );
156 |       }
157 | 
158 |       // Request limit of 5
159 |       const related = await manager.getRelated(entry1, 5);
160 | 
161 |       // Should be limited to 5 (deduplicated)
162 |       expect(related.length).toBeLessThanOrEqual(5);
163 | 
164 |       // Should not include entry1 itself
165 |       const names = related.map((r) => r.data.name);
166 |       expect(names).not.toContain("Entry 1");
167 |     });
168 | 
169 |     it("should handle entry without tags gracefully (line 189)", async () => {
170 |       const entryNoTags = await manager.remember(
171 |         "analysis",
172 |         { name: "No Tags" },
173 |         { projectId: "proj-001" },
174 |       );
175 | 
176 |       await manager.remember(
177 |         "analysis",
178 |         { name: "Also No Tags" },
179 |         { projectId: "proj-001" },
180 |       );
181 | 
182 |       // Should still find related by project
183 |       const related = await manager.getRelated(entryNoTags, 10);
184 |       expect(related.length).toBeGreaterThan(0);
185 |     });
186 | 
187 |     it("should handle entry with empty tags array (line 189)", async () => {
188 |       const entryEmptyTags = await manager.remember(
189 |         "analysis",
190 |         { name: "Empty Tags" },
191 |         {
192 |           projectId: "proj-001",
193 |           tags: [],
194 |         },
195 |       );
196 | 
197 |       await manager.remember(
198 |         "analysis",
199 |         { name: "Other Entry" },
200 |         { projectId: "proj-001" },
201 |       );
202 | 
203 |       const related = await manager.getRelated(entryEmptyTags, 10);
204 |       expect(related.length).toBeGreaterThan(0);
205 |     });
206 |   });
207 | 
208 |   describe("CSV Export (lines 381-398)", () => {
209 |     it("should export memories as CSV format", async () => {
210 |       manager.setContext({ projectId: "csv-proj-001" });
211 | 
212 |       await manager.remember(
213 |         "analysis",
214 |         { test: "data1" },
215 |         {
216 |           repository: "github.com/test/repo1",
217 |           ssg: "jekyll",
218 |         },
219 |       );
220 | 
221 |       manager.setContext({ projectId: "csv-proj-002" });
222 | 
223 |       await manager.remember(
224 |         "recommendation",
225 |         { test: "data2" },
226 |         {
227 |           repository: "github.com/test/repo2",
228 |           ssg: "hugo",
229 |         },
230 |       );
231 | 
232 |       // Export as CSV
233 |       const csvData = await manager.export("csv");
234 | 
235 |       // Verify CSV structure
236 |       expect(csvData).toContain("id,timestamp,type,projectId,repository,ssg");
237 |       expect(csvData).toContain("csv-proj-001");
238 |       expect(csvData).toContain("csv-proj-002");
239 |       expect(csvData).toContain("github.com/test/repo1");
240 |       expect(csvData).toContain("github.com/test/repo2");
241 |       expect(csvData).toContain("jekyll");
242 |       expect(csvData).toContain("hugo");
243 | 
244 |       // Verify rows are comma-separated
245 |       const lines = csvData.split("\n").filter((l) => l.trim());
246 |       expect(lines.length).toBeGreaterThanOrEqual(3); // header + 2 rows
247 | 
248 |       // Each line should have the same number of commas
249 |       const headerCommas = (lines[0].match(/,/g) || []).length;
250 |       for (let i = 1; i < lines.length; i++) {
251 |         const rowCommas = (lines[i].match(/,/g) || []).length;
252 |         expect(rowCommas).toBe(headerCommas);
253 |       }
254 |     });
255 | 
256 |     it("should export memories for specific project only", async () => {
257 |       manager.setContext({ projectId: "project-a" });
258 |       await manager.remember("analysis", { project: "A" }, {});
259 | 
260 |       manager.setContext({ projectId: "project-b" });
261 |       await manager.remember("analysis", { project: "B" }, {});
262 | 
263 |       // Export only project-a
264 |       const csvData = await manager.export("csv", "project-a");
265 | 
266 |       expect(csvData).toContain("project-a");
267 |       expect(csvData).not.toContain("project-b");
268 |     });
269 | 
270 |     it("should handle missing metadata fields in CSV export (lines 393-395)", async () => {
271 |       // Create entry with minimal metadata
272 |       await manager.remember("analysis", { test: "minimal" }, {});
273 | 
274 |       const csvData = await manager.export("csv");
275 | 
276 |       // Should have empty fields for missing metadata
277 |       const lines = csvData.split("\n");
278 |       expect(lines.length).toBeGreaterThan(1);
279 | 
280 |       // Verify header
281 |       expect(lines[0]).toContain("id,timestamp,type,projectId,repository,ssg");
282 | 
283 |       // Data row should have appropriate number of commas (empty fields)
284 |       const dataRow = lines[1];
285 |       const headerCommas = (lines[0].match(/,/g) || []).length;
286 |       const dataCommas = (dataRow.match(/,/g) || []).length;
287 |       expect(dataCommas).toBe(headerCommas);
288 |     });
289 | 
290 |     it("should export as JSON by default", async () => {
291 |       await manager.remember(
292 |         "analysis",
293 |         { json: "test" },
294 |         { projectId: "json-proj" },
295 |       );
296 | 
297 |       const jsonData = await manager.export("json");
298 | 
299 |       const parsed = JSON.parse(jsonData);
300 |       expect(Array.isArray(parsed)).toBe(true);
301 |       expect(parsed.length).toBeGreaterThan(0);
302 |       expect(parsed[0].data.json).toBe("test");
303 |     });
304 |   });
305 | 
306 |   describe("CSV Import (lines 409-428)", () => {
307 |     it("should import memories from CSV format", async () => {
308 |       // Create CSV data
309 |       const csvData = `id,timestamp,type,projectId,repository,ssg
310 | mem-001,2024-01-01T00:00:00.000Z,analysis,proj-csv-001,github.com/test/repo1,jekyll
311 | mem-002,2024-01-02T00:00:00.000Z,recommendation,proj-csv-002,github.com/test/repo2,hugo
312 | mem-003,2024-01-03T00:00:00.000Z,deployment,proj-csv-003,github.com/test/repo3,mkdocs`;
313 | 
314 |       const imported = await manager.import(csvData, "csv");
315 | 
316 |       expect(imported).toBe(3);
317 | 
318 |       // Verify entries were imported
319 |       const recalled1 = await manager.recall("mem-001");
320 |       expect(recalled1).not.toBeNull();
321 |       expect(recalled1?.type).toBe("analysis");
322 |       expect(recalled1?.metadata.projectId).toBe("proj-csv-001");
323 |       expect(recalled1?.metadata.ssg).toBe("jekyll");
324 | 
325 |       const recalled2 = await manager.recall("mem-002");
326 |       expect(recalled2).not.toBeNull();
327 |       expect(recalled2?.type).toBe("recommendation");
328 |     });
329 | 
330 |     it("should skip malformed CSV rows (line 414)", async () => {
331 |       // CSV with mismatched column counts
332 |       const csvData = `id,timestamp,type,projectId,repository,ssg
333 | mem-001,2024-01-01T00:00:00.000Z,analysis,proj-001,github.com/test/repo,jekyll
334 | mem-002,2024-01-02T00:00:00.000Z,recommendation
335 | mem-003,2024-01-03T00:00:00.000Z,deployment,proj-003,github.com/test/repo3,mkdocs`;
336 | 
337 |       const imported = await manager.import(csvData, "csv");
338 | 
339 |       // Should import 2 (skipping the malformed row)
340 |       expect(imported).toBe(2);
341 | 
342 |       // Verify valid entries were imported
343 |       const recalled1 = await manager.recall("mem-001");
344 |       expect(recalled1).not.toBeNull();
345 | 
346 |       // Malformed entry should not be imported
347 |       const recalled2 = await manager.recall("mem-002");
348 |       expect(recalled2).toBeNull();
349 | 
350 |       const recalled3 = await manager.recall("mem-003");
351 |       expect(recalled3).not.toBeNull();
352 |     });
353 | 
354 |     it("should import memories from JSON format", async () => {
355 |       const jsonData = JSON.stringify([
356 |         {
357 |           id: "json-001",
358 |           timestamp: "2024-01-01T00:00:00.000Z",
359 |           type: "analysis",
360 |           data: { test: "json-import" },
361 |           metadata: { projectId: "json-proj" },
362 |         },
363 |       ]);
364 | 
365 |       const imported = await manager.import(jsonData, "json");
366 | 
367 |       expect(imported).toBe(1);
368 | 
369 |       const recalled = await manager.recall("json-001");
370 |       expect(recalled).not.toBeNull();
371 |       expect(recalled?.data.test).toBe("json-import");
372 |     });
373 | 
374 |     it("should emit import-complete event (line 437)", async () => {
375 |       let eventEmitted = false;
376 |       let importedCount = 0;
377 | 
378 |       manager.on("import-complete", (count) => {
379 |         eventEmitted = true;
380 |         importedCount = count;
381 |       });
382 | 
383 |       const jsonData = JSON.stringify([
384 |         {
385 |           id: "event-001",
386 |           timestamp: "2024-01-01T00:00:00.000Z",
387 |           type: "analysis",
388 |           data: {},
389 |           metadata: {},
390 |         },
391 |       ]);
392 | 
393 |       await manager.import(jsonData, "json");
394 | 
395 |       expect(eventEmitted).toBe(true);
396 |       expect(importedCount).toBe(1);
397 |     });
398 | 
399 |     it("should handle empty CSV import gracefully", async () => {
400 |       const csvData = `id,timestamp,type,projectId,repository,ssg`;
401 | 
402 |       const imported = await manager.import(csvData, "csv");
403 | 
404 |       expect(imported).toBe(0);
405 |     });
406 | 
407 |     it("should handle empty JSON import gracefully", async () => {
408 |       const jsonData = JSON.stringify([]);
409 | 
410 |       const imported = await manager.import(jsonData, "json");
411 | 
412 |       expect(imported).toBe(0);
413 |     });
414 |   });
415 | 
416 |   describe("Export and Import Round-trip", () => {
417 |     it("should maintain data integrity through CSV round-trip", async () => {
418 |       // Create test data
419 |       manager.setContext({
420 |         projectId: "roundtrip-proj",
421 |         repository: "github.com/test/roundtrip",
422 |       });
423 |       const originalEntry = await manager.remember(
424 |         "analysis",
425 |         { roundtrip: "test" },
426 |         {
427 |           ssg: "docusaurus",
428 |         },
429 |       );
430 | 
431 |       // Export as CSV
432 |       const csvData = await manager.export("csv");
433 | 
434 |       // Create new manager and import
435 |       const tempDir2 = path.join(
436 |         os.tmpdir(),
437 |         `manager-roundtrip-${Date.now()}`,
438 |       );
439 |       await fs.mkdir(tempDir2, { recursive: true });
440 |       const manager2 = new MemoryManager(tempDir2);
441 |       await manager2.initialize();
442 | 
443 |       const imported = await manager2.import(csvData, "csv");
444 |       expect(imported).toBeGreaterThan(0);
445 | 
446 |       // Verify data matches
447 |       const recalled = await manager2.recall(originalEntry.id);
448 |       expect(recalled).not.toBeNull();
449 |       expect(recalled?.type).toBe(originalEntry.type);
450 |       expect(recalled?.metadata.projectId).toBe(
451 |         originalEntry.metadata.projectId,
452 |       );
453 |       expect(recalled?.metadata.ssg).toBe(originalEntry.metadata.ssg);
454 | 
455 |       await manager2.close();
456 |       await fs.rm(tempDir2, { recursive: true, force: true });
457 |     });
458 | 
459 |     it("should maintain data integrity through JSON round-trip", async () => {
460 |       // Create test data with complex structure
461 |       manager.setContext({ projectId: "json-roundtrip" });
462 |       const originalEntry = await manager.remember(
463 |         "analysis",
464 |         {
465 |           complex: "data",
466 |           nested: { value: 123 },
467 |           array: [1, 2, 3],
468 |         },
469 |         {
470 |           tags: ["tag1", "tag2"],
471 |         },
472 |       );
473 | 
474 |       // Export as JSON
475 |       const jsonData = await manager.export("json");
476 | 
477 |       // Create new manager and import
478 |       const tempDir2 = path.join(
479 |         os.tmpdir(),
480 |         `manager-json-roundtrip-${Date.now()}`,
481 |       );
482 |       await fs.mkdir(tempDir2, { recursive: true });
483 |       const manager2 = new MemoryManager(tempDir2);
484 |       await manager2.initialize();
485 | 
486 |       const imported = await manager2.import(jsonData, "json");
487 |       expect(imported).toBeGreaterThan(0);
488 | 
489 |       // Verify complex data maintained
490 |       const recalled = await manager2.recall(originalEntry.id);
491 |       expect(recalled).not.toBeNull();
492 |       expect(recalled?.data).toEqual(originalEntry.data);
493 |       expect(recalled?.metadata.tags).toEqual(originalEntry.metadata.tags);
494 | 
495 |       await manager2.close();
496 |       await fs.rm(tempDir2, { recursive: true, force: true });
497 |     });
498 |   });
499 | });
500 | 
```

--------------------------------------------------------------------------------
/src/tools/evaluate-readme-health.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { promises as fs } from "fs";
  3 | import path from "path";
  4 | import { formatMCPResponse } from "../types/api.js";
  5 | 
  6 | // Input validation schema
  7 | const EvaluateReadmeHealthSchema = z.object({
  8 |   readme_path: z.string().min(1, "README path is required"),
  9 |   project_type: z
 10 |     .enum([
 11 |       "community_library",
 12 |       "enterprise_tool",
 13 |       "personal_project",
 14 |       "documentation",
 15 |     ])
 16 |     .optional()
 17 |     .default("community_library"),
 18 |   repository_path: z.string().optional(),
 19 | });
 20 | 
 21 | // Input type that matches what users actually pass (project_type is optional)
 22 | export interface EvaluateReadmeHealthInput {
 23 |   readme_path: string;
 24 |   project_type?:
 25 |     | "community_library"
 26 |     | "enterprise_tool"
 27 |     | "personal_project"
 28 |     | "documentation";
 29 |   repository_path?: string;
 30 | }
 31 | 
 32 | // Health score interfaces
 33 | interface HealthScoreComponent {
 34 |   name: string;
 35 |   score: number;
 36 |   maxScore: number;
 37 |   details: HealthCheckDetail[];
 38 | }
 39 | 
 40 | interface HealthCheckDetail {
 41 |   check: string;
 42 |   passed: boolean;
 43 |   points: number;
 44 |   maxPoints: number;
 45 |   recommendation?: string;
 46 | }
 47 | 
 48 | interface ReadmeHealthReport {
 49 |   overallScore: number;
 50 |   maxScore: number;
 51 |   grade: "A" | "B" | "C" | "D" | "F";
 52 |   components: {
 53 |     communityHealth: HealthScoreComponent;
 54 |     accessibility: HealthScoreComponent;
 55 |     onboarding: HealthScoreComponent;
 56 |     contentQuality: HealthScoreComponent;
 57 |   };
 58 |   recommendations: string[];
 59 |   strengths: string[];
 60 |   criticalIssues: string[];
 61 |   estimatedImprovementTime: string;
 62 | }
 63 | 
 64 | export async function evaluateReadmeHealth(input: EvaluateReadmeHealthInput) {
 65 |   const startTime = Date.now();
 66 |   try {
 67 |     // Validate input
 68 |     const validatedInput = EvaluateReadmeHealthSchema.parse(input);
 69 | 
 70 |     // Read README file
 71 |     const readmePath = path.resolve(validatedInput.readme_path);
 72 |     const readmeContent = await fs.readFile(readmePath, "utf-8");
 73 | 
 74 |     // Get repository context if available
 75 |     let repoContext: any = null;
 76 |     if (validatedInput.repository_path) {
 77 |       repoContext = await analyzeRepositoryContext(
 78 |         validatedInput.repository_path,
 79 |       );
 80 |     }
 81 | 
 82 |     // Evaluate all health components
 83 |     const communityHealth = evaluateCommunityHealth(readmeContent, repoContext);
 84 |     const accessibility = evaluateAccessibility(readmeContent);
 85 |     const onboarding = evaluateOnboarding(
 86 |       readmeContent,
 87 |       validatedInput.project_type,
 88 |     );
 89 |     const contentQuality = evaluateContentQuality(readmeContent);
 90 | 
 91 |     // Calculate overall score
 92 |     const totalScore =
 93 |       communityHealth.score +
 94 |       accessibility.score +
 95 |       onboarding.score +
 96 |       contentQuality.score;
 97 |     const maxTotalScore =
 98 |       communityHealth.maxScore +
 99 |       accessibility.maxScore +
100 |       onboarding.maxScore +
101 |       contentQuality.maxScore;
102 |     const percentage = (totalScore / maxTotalScore) * 100;
103 | 
104 |     // Generate grade
105 |     const grade = getGrade(percentage);
106 | 
107 |     // Generate recommendations and insights
108 |     const recommendations = generateHealthRecommendations(
109 |       [communityHealth, accessibility, onboarding, contentQuality],
110 |       "general",
111 |     );
112 |     const strengths = identifyStrengths([
113 |       communityHealth,
114 |       accessibility,
115 |       onboarding,
116 |       contentQuality,
117 |     ]);
118 |     const criticalIssues = identifyCriticalIssues([
119 |       communityHealth,
120 |       accessibility,
121 |       onboarding,
122 |       contentQuality,
123 |     ]);
124 | 
125 |     const report: ReadmeHealthReport = {
126 |       overallScore: Math.round(percentage),
127 |       maxScore: 100,
128 |       grade,
129 |       components: {
130 |         communityHealth,
131 |         accessibility,
132 |         onboarding,
133 |         contentQuality,
134 |       },
135 |       recommendations,
136 |       strengths,
137 |       criticalIssues,
138 |       estimatedImprovementTime: estimateImprovementTime(
139 |         recommendations.length,
140 |         criticalIssues.length,
141 |       ),
142 |     };
143 | 
144 |     const response = {
145 |       readmePath: validatedInput.readme_path,
146 |       projectType: validatedInput.project_type,
147 |       healthReport: report,
148 |       summary: generateSummary(report),
149 |       nextSteps: generateNextSteps(report),
150 |     };
151 | 
152 |     return formatMCPResponse({
153 |       success: true,
154 |       data: response,
155 |       metadata: {
156 |         toolVersion: "1.0.0",
157 |         executionTime: Date.now() - startTime,
158 |         timestamp: new Date().toISOString(),
159 |       },
160 |     });
161 |   } catch (error) {
162 |     return formatMCPResponse({
163 |       success: false,
164 |       error: {
165 |         code: "README_HEALTH_EVALUATION_FAILED",
166 |         message: `Failed to evaluate README health: ${error}`,
167 |         resolution: "Ensure README path is valid and file is readable",
168 |       },
169 |       metadata: {
170 |         toolVersion: "1.0.0",
171 |         executionTime: Date.now() - startTime,
172 |         timestamp: new Date().toISOString(),
173 |       },
174 |     });
175 |   }
176 | }
177 | 
178 | function evaluateCommunityHealth(
179 |   content: string,
180 |   _repoContext: any,
181 | ): HealthScoreComponent {
182 |   const checks: HealthCheckDetail[] = [
183 |     {
184 |       check: "Code of Conduct linked",
185 |       passed: /code.of.conduct|conduct\.md|\.github\/code_of_conduct/i.test(
186 |         content,
187 |       ),
188 |       points: 0,
189 |       maxPoints: 5,
190 |       recommendation:
191 |         "Add a link to your Code of Conduct to establish community standards",
192 |     },
193 |     {
194 |       check: "Contributing guidelines visible",
195 |       passed: /contributing|contribute\.md|\.github\/contributing/i.test(
196 |         content,
197 |       ),
198 |       points: 0,
199 |       maxPoints: 5,
200 |       recommendation:
201 |         "Include contributing guidelines to help new contributors get started",
202 |     },
203 |     {
204 |       check: "Issue/PR templates mentioned",
205 |       passed:
206 |         /issue.template|pull.request.template|\.github\/issue_template|\.github\/pull_request_template/i.test(
207 |           content,
208 |         ),
209 |       points: 0,
210 |       maxPoints: 5,
211 |       recommendation:
212 |         "Reference issue and PR templates to streamline contributions",
213 |     },
214 |     {
215 |       check: "Security policy linked",
216 |       passed: /security\.md|security.policy|\.github\/security/i.test(content),
217 |       points: 0,
218 |       maxPoints: 5,
219 |       recommendation:
220 |         "Add a security policy to handle vulnerability reports responsibly",
221 |     },
222 |     {
223 |       check: "Support channels provided",
224 |       passed: /support|help|discord|slack|discussions|forum|community/i.test(
225 |         content,
226 |       ),
227 |       points: 0,
228 |       maxPoints: 5,
229 |       recommendation: "Provide clear support channels for users seeking help",
230 |     },
231 |   ];
232 | 
233 |   // Award points for passed checks
234 |   checks.forEach((check) => {
235 |     if (check.passed) {
236 |       check.points = check.maxPoints;
237 |     }
238 |   });
239 | 
240 |   const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
241 |   const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
242 | 
243 |   return {
244 |     name: "Community Health",
245 |     score: totalScore,
246 |     maxScore,
247 |     details: checks,
248 |   };
249 | }
250 | 
251 | function evaluateAccessibility(content: string): HealthScoreComponent {
252 |   const lines = content.split("\n");
253 |   const headings = lines.filter((line) => line.trim().startsWith("#"));
254 |   const images = content.match(/!\[.*?\]\(.*?\)/g) || [];
255 | 
256 |   const checks: HealthCheckDetail[] = [
257 |     {
258 |       check: "Scannable structure with proper spacing",
259 |       passed: content.includes("\n\n") && lines.length > 10,
260 |       points: 0,
261 |       maxPoints: 5,
262 |       recommendation: "Use proper spacing and breaks to make content scannable",
263 |     },
264 |     {
265 |       check: "Clear heading hierarchy",
266 |       passed: headings.length >= 3 && headings.some((h) => h.startsWith("##")),
267 |       points: 0,
268 |       maxPoints: 5,
269 |       recommendation:
270 |         "Use proper heading hierarchy (H1, H2, H3) to structure content",
271 |     },
272 |     {
273 |       check: "Alt text for images",
274 |       passed:
275 |         images.length === 0 || images.every((img) => !img.includes("![](")),
276 |       points: 0,
277 |       maxPoints: 5,
278 |       recommendation:
279 |         "Add descriptive alt text for all images for screen readers",
280 |     },
281 |     {
282 |       check: "Inclusive language",
283 |       passed: !/\b(guys|blacklist|whitelist|master|slave)\b/i.test(content),
284 |       points: 0,
285 |       maxPoints: 5,
286 |       recommendation:
287 |         'Use inclusive language (e.g., "team" instead of "guys", "allowlist/blocklist")',
288 |     },
289 |   ];
290 | 
291 |   // Award points for passed checks
292 |   checks.forEach((check) => {
293 |     if (check.passed) {
294 |       check.points = check.maxPoints;
295 |     }
296 |   });
297 | 
298 |   const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
299 |   const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
300 | 
301 |   return {
302 |     name: "Accessibility",
303 |     score: totalScore,
304 |     maxScore,
305 |     details: checks,
306 |   };
307 | }
308 | 
309 | function evaluateOnboarding(
310 |   content: string,
311 |   _projectType: string,
312 | ): HealthScoreComponent {
313 |   const checks: HealthCheckDetail[] = [
314 |     {
315 |       check: "Quick start section",
316 |       passed: /quick.start|getting.started|installation|setup/i.test(content),
317 |       points: 0,
318 |       maxPoints: 5,
319 |       recommendation:
320 |         "Add a quick start section to help users get up and running fast",
321 |     },
322 |     {
323 |       check: "Prerequisites clearly listed",
324 |       passed: /prerequisites|requirements|dependencies|before.you.begin/i.test(
325 |         content,
326 |       ),
327 |       points: 0,
328 |       maxPoints: 5,
329 |       recommendation: "Clearly list all prerequisites and system requirements",
330 |     },
331 |     {
332 |       check: "First contribution guide",
333 |       passed: /first.contribution|new.contributor|beginner|newcomer/i.test(
334 |         content,
335 |       ),
336 |       points: 0,
337 |       maxPoints: 5,
338 |       recommendation:
339 |         "Include guidance specifically for first-time contributors",
340 |     },
341 |     {
342 |       check: "Good first issues mentioned",
343 |       passed: /good.first.issue|beginner.friendly|easy.pick|help.wanted/i.test(
344 |         content,
345 |       ),
346 |       points: 0,
347 |       maxPoints: 5,
348 |       recommendation: "Mention good first issues or beginner-friendly tasks",
349 |     },
350 |   ];
351 | 
352 |   // Award points for passed checks
353 |   checks.forEach((check) => {
354 |     if (check.passed) {
355 |       check.points = check.maxPoints;
356 |     }
357 |   });
358 | 
359 |   const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
360 |   const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
361 | 
362 |   return {
363 |     name: "Onboarding",
364 |     score: totalScore,
365 |     maxScore,
366 |     details: checks,
367 |   };
368 | }
369 | 
370 | function evaluateContentQuality(content: string): HealthScoreComponent {
371 |   const wordCount = content.split(/\s+/).length;
372 |   const codeBlocks = (content.match(/```/g) || []).length / 2;
373 |   const links = (content.match(/\[.*?\]\(.*?\)/g) || []).length;
374 | 
375 |   const checks: HealthCheckDetail[] = [
376 |     {
377 |       check: "Adequate content length",
378 |       passed: wordCount >= 50 && wordCount <= 2000,
379 |       points: 0,
380 |       maxPoints: 5,
381 |       recommendation:
382 |         "Maintain optimal README length (50-2000 words) for readability",
383 |     },
384 |     {
385 |       check: "Code examples provided",
386 |       passed: codeBlocks >= 2,
387 |       points: 0,
388 |       maxPoints: 5,
389 |       recommendation: "Include practical code examples to demonstrate usage",
390 |     },
391 |     {
392 |       check: "External links present",
393 |       passed: links >= 3,
394 |       points: 0,
395 |       maxPoints: 5,
396 |       recommendation:
397 |         "Add relevant external links (docs, demos, related projects)",
398 |     },
399 |     {
400 |       check: "Project description clarity",
401 |       passed: /## |### /.test(content) && content.length > 500,
402 |       points: 0,
403 |       maxPoints: 5,
404 |       recommendation:
405 |         "Provide clear, detailed project description with proper structure",
406 |     },
407 |   ];
408 | 
409 |   // Award points for passed checks
410 |   checks.forEach((check) => {
411 |     if (check.passed) {
412 |       check.points = check.maxPoints;
413 |     }
414 |   });
415 | 
416 |   const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
417 |   const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
418 | 
419 |   return {
420 |     name: "Content Quality",
421 |     score: totalScore,
422 |     maxScore,
423 |     details: checks,
424 |   };
425 | }
426 | 
427 | async function analyzeRepositoryContext(repoPath: string): Promise<any> {
428 |   try {
429 |     const repoDir = path.resolve(repoPath);
430 |     const files = await fs.readdir(repoDir);
431 | 
432 |     return {
433 |       hasCodeOfConduct: files.includes("CODE_OF_CONDUCT.md"),
434 |       hasContributing: files.includes("CONTRIBUTING.md"),
435 |       hasSecurityPolicy: files.includes("SECURITY.md"),
436 |       hasGithubDir: files.includes(".github"),
437 |       packageJson: files.includes("package.json"),
438 |     };
439 |   } catch (error) {
440 |     return null;
441 |   }
442 | }
443 | 
444 | function getGrade(percentage: number): "A" | "B" | "C" | "D" | "F" {
445 |   if (percentage >= 90) return "A";
446 |   if (percentage >= 80) return "B";
447 |   if (percentage >= 70) return "C";
448 |   if (percentage >= 60) return "D";
449 |   return "F";
450 | }
451 | 
452 | function generateHealthRecommendations(
453 |   analysis: any[],
454 |   _projectType: string,
455 | ): string[] {
456 |   const recommendations: string[] = [];
457 | 
458 |   analysis.forEach((component: any) => {
459 |     component.details.forEach((detail: any) => {
460 |       if (detail.points < detail.maxPoints) {
461 |         recommendations.push(`${component.name}: ${detail.recommendation}`);
462 |       }
463 |     });
464 |   });
465 | 
466 |   return recommendations.slice(0, 10); // Top 10 recommendations
467 | }
468 | 
469 | function identifyStrengths(components: HealthScoreComponent[]): string[] {
470 |   const strengths: string[] = [];
471 | 
472 |   components.forEach((component) => {
473 |     const passedChecks = component.details.filter((detail) => detail.passed);
474 |     if (passedChecks.length > component.details.length / 2) {
475 |       strengths.push(
476 |         `Strong ${component.name.toLowerCase()}: ${passedChecks
477 |           .map((c) => c.check.toLowerCase())
478 |           .join(", ")}`,
479 |       );
480 |     }
481 |   });
482 | 
483 |   return strengths;
484 | }
485 | 
486 | function identifyCriticalIssues(components: HealthScoreComponent[]): string[] {
487 |   const critical: string[] = [];
488 | 
489 |   components.forEach((component) => {
490 |     if (component.score < component.maxScore * 0.3) {
491 |       // Less than 30% score
492 |       critical.push(
493 |         `Critical: Poor ${component.name.toLowerCase()} (${component.score}/${
494 |           component.maxScore
495 |         } points)`,
496 |       );
497 |     }
498 |   });
499 | 
500 |   return critical;
501 | }
502 | 
503 | function estimateImprovementTime(
504 |   recommendationCount: number,
505 |   criticalCount: number,
506 | ): string {
507 |   const baseTime = recommendationCount * 15; // 15 minutes per recommendation
508 |   const criticalTime = criticalCount * 30; // 30 minutes per critical issue
509 |   const totalMinutes = baseTime + criticalTime;
510 | 
511 |   if (totalMinutes < 60) return `${totalMinutes} minutes`;
512 |   if (totalMinutes < 480) return `${Math.round(totalMinutes / 60)} hours`;
513 |   return `${Math.round(totalMinutes / 480)} days`;
514 | }
515 | 
516 | function generateSummary(report: ReadmeHealthReport): string {
517 |   const { overallScore, grade, components } = report;
518 | 
519 |   const componentScores = Object.values(components)
520 |     .map((c) => `${c.name}: ${c.score}/${c.maxScore}`)
521 |     .join(", ");
522 | 
523 |   return `README Health Score: ${overallScore}/100 (Grade ${grade}). Component breakdown: ${componentScores}. ${report.criticalIssues.length} critical issues identified.`;
524 | }
525 | 
526 | function generateNextSteps(report: ReadmeHealthReport): string[] {
527 |   const steps: string[] = [];
528 | 
529 |   if (report.criticalIssues.length > 0) {
530 |     steps.push(
531 |       "Address critical issues first to establish baseline community health",
532 |     );
533 |   }
534 | 
535 |   if (report.recommendations.length > 0) {
536 |     steps.push(
537 |       `Implement top ${Math.min(
538 |         3,
539 |         report.recommendations.length,
540 |       )} recommendations for quick wins`,
541 |     );
542 |   }
543 | 
544 |   if (report.overallScore < 85) {
545 |     steps.push("Target 85+ health score for optimal community engagement");
546 |   }
547 | 
548 |   steps.push("Re-evaluate after improvements to track progress");
549 | 
550 |   return steps;
551 | }
552 | 
```

--------------------------------------------------------------------------------
/tests/tools/analyze-deployments.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for Phase 2.4: Deployment Analytics and Insights
  3 |  * Tests the analyze_deployments tool with comprehensive pattern analysis
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
  7 | import { promises as fs } from "fs";
  8 | import { join } from "path";
  9 | import { tmpdir } from "os";
 10 | import {
 11 |   initializeKnowledgeGraph,
 12 |   getKnowledgeGraph,
 13 |   createOrUpdateProject,
 14 |   trackDeployment,
 15 | } from "../../src/memory/kg-integration.js";
 16 | import { analyzeDeployments } from "../../src/tools/analyze-deployments.js";
 17 | 
 18 | describe("analyzeDeployments (Phase 2.4)", () => {
 19 |   let testDir: string;
 20 |   let originalEnv: string | undefined;
 21 | 
 22 |   beforeEach(async () => {
 23 |     // Create temporary test directory
 24 |     testDir = join(tmpdir(), `analyze-deployments-test-${Date.now()}`);
 25 |     await fs.mkdir(testDir, { recursive: true });
 26 | 
 27 |     // Set environment variable for storage
 28 |     originalEnv = process.env.DOCUMCP_STORAGE_DIR;
 29 |     process.env.DOCUMCP_STORAGE_DIR = testDir;
 30 | 
 31 |     // Initialize KG
 32 |     await initializeKnowledgeGraph(testDir);
 33 |   });
 34 | 
 35 |   afterEach(async () => {
 36 |     // Restore environment
 37 |     if (originalEnv) {
 38 |       process.env.DOCUMCP_STORAGE_DIR = originalEnv;
 39 |     } else {
 40 |       delete process.env.DOCUMCP_STORAGE_DIR;
 41 |     }
 42 | 
 43 |     // Clean up test directory
 44 |     try {
 45 |       await fs.rm(testDir, { recursive: true, force: true });
 46 |     } catch (error) {
 47 |       console.warn("Failed to clean up test directory:", error);
 48 |     }
 49 |   });
 50 | 
 51 |   /**
 52 |    * Helper function to create sample deployment data
 53 |    */
 54 |   const createSampleDeployments = async () => {
 55 |     const timestamp = new Date().toISOString();
 56 | 
 57 |     // Create 3 projects
 58 |     const project1 = await createOrUpdateProject({
 59 |       id: "project1",
 60 |       timestamp,
 61 |       path: "/test/project1",
 62 |       projectName: "Docusaurus Site",
 63 |       structure: {
 64 |         totalFiles: 50,
 65 |         languages: { typescript: 30, javascript: 20 },
 66 |         hasTests: true,
 67 |         hasCI: true,
 68 |         hasDocs: true,
 69 |       },
 70 |     });
 71 | 
 72 |     const project2 = await createOrUpdateProject({
 73 |       id: "project2",
 74 |       timestamp,
 75 |       path: "/test/project2",
 76 |       projectName: "Hugo Blog",
 77 |       structure: {
 78 |         totalFiles: 30,
 79 |         languages: { go: 15, html: 15 },
 80 |         hasTests: false,
 81 |         hasCI: true,
 82 |         hasDocs: true,
 83 |       },
 84 |     });
 85 | 
 86 |     const project3 = await createOrUpdateProject({
 87 |       id: "project3",
 88 |       timestamp,
 89 |       path: "/test/project3",
 90 |       projectName: "MkDocs Docs",
 91 |       structure: {
 92 |         totalFiles: 40,
 93 |         languages: { python: 25, markdown: 15 },
 94 |         hasTests: true,
 95 |         hasCI: true,
 96 |         hasDocs: true,
 97 |       },
 98 |     });
 99 | 
100 |     // Track successful deployments
101 |     await trackDeployment(project1.id, "docusaurus", true, {
102 |       buildTime: 25000,
103 |     });
104 |     await trackDeployment(project1.id, "docusaurus", true, {
105 |       buildTime: 23000,
106 |     });
107 | 
108 |     await trackDeployment(project2.id, "hugo", true, { buildTime: 15000 });
109 |     await trackDeployment(project2.id, "hugo", true, { buildTime: 14000 });
110 |     await trackDeployment(project2.id, "hugo", true, { buildTime: 16000 });
111 | 
112 |     await trackDeployment(project3.id, "mkdocs", true, { buildTime: 30000 });
113 |     await trackDeployment(project3.id, "mkdocs", false, {
114 |       errorMessage: "Build failed",
115 |     });
116 | 
117 |     return { project1, project2, project3 };
118 |   };
119 | 
120 |   describe("Full Report Analysis", () => {
121 |     it("should generate comprehensive analytics report with no data", async () => {
122 |       const result = await analyzeDeployments({});
123 | 
124 |       const content = result.content[0];
125 |       expect(content.type).toBe("text");
126 |       const data = JSON.parse(content.text);
127 | 
128 |       expect(data.summary).toBeDefined();
129 |       expect(data.summary.totalProjects).toBe(0);
130 |       expect(data.summary.totalDeployments).toBe(0);
131 |       expect(data.patterns).toEqual([]);
132 |       // With 0 deployments, we get a warning insight about low success rate
133 |       expect(Array.isArray(data.insights)).toBe(true);
134 |       expect(data.recommendations).toBeDefined();
135 |     });
136 | 
137 |     it("should generate comprehensive analytics report with sample data", async () => {
138 |       await createSampleDeployments();
139 | 
140 |       const result = await analyzeDeployments({
141 |         analysisType: "full_report",
142 |       });
143 | 
144 |       const content = result.content[0];
145 |       expect(content.type).toBe("text");
146 |       const data = JSON.parse(content.text);
147 | 
148 |       // Verify summary
149 |       expect(data.summary).toBeDefined();
150 |       expect(data.summary.totalProjects).toBe(3);
151 |       // Each project has 1 configuration node, so 3 total deployments tracked
152 |       expect(data.summary.totalDeployments).toBeGreaterThanOrEqual(3);
153 |       expect(data.summary.overallSuccessRate).toBeGreaterThan(0);
154 |       expect(data.summary.mostUsedSSG).toBeDefined();
155 | 
156 |       // Verify patterns
157 |       expect(data.patterns).toBeDefined();
158 |       expect(data.patterns.length).toBeGreaterThan(0);
159 |       expect(data.patterns[0]).toHaveProperty("ssg");
160 |       expect(data.patterns[0]).toHaveProperty("totalDeployments");
161 |       expect(data.patterns[0]).toHaveProperty("successRate");
162 | 
163 |       // Verify insights and recommendations
164 |       expect(data.insights).toBeDefined();
165 |       expect(data.recommendations).toBeDefined();
166 |     });
167 | 
168 |     it("should include insights about high success rates", async () => {
169 |       await createSampleDeployments();
170 | 
171 |       const result = await analyzeDeployments({
172 |         analysisType: "full_report",
173 |       });
174 | 
175 |       const content = result.content[0];
176 |       const data = JSON.parse(content.text);
177 | 
178 |       // Should have success insights for docusaurus and hugo
179 |       const successInsights = data.insights.filter(
180 |         (i: any) => i.type === "success",
181 |       );
182 |       expect(successInsights.length).toBeGreaterThan(0);
183 |     });
184 |   });
185 | 
186 |   describe("SSG Statistics Analysis", () => {
187 |     it("should return error for non-existent SSG", async () => {
188 |       const result = await analyzeDeployments({
189 |         analysisType: "ssg_stats",
190 |         ssg: "nonexistent",
191 |       });
192 | 
193 |       const content = result.content[0];
194 |       const data = JSON.parse(content.text);
195 | 
196 |       // Should return error response when SSG has no data
197 |       expect(data.success).toBe(false);
198 |       expect(data.error).toBeDefined();
199 |     });
200 | 
201 |     it("should return statistics for specific SSG", async () => {
202 |       await createSampleDeployments();
203 | 
204 |       const result = await analyzeDeployments({
205 |         analysisType: "ssg_stats",
206 |         ssg: "docusaurus",
207 |       });
208 | 
209 |       const content = result.content[0];
210 |       expect(content.type).toBe("text");
211 |       const data = JSON.parse(content.text);
212 | 
213 |       expect(data.ssg).toBe("docusaurus");
214 |       // project1 has 2 deployments with docusaurus
215 |       expect(data.totalDeployments).toBeGreaterThanOrEqual(1);
216 |       expect(data.successfulDeployments).toBeGreaterThanOrEqual(1);
217 |       expect(data.successRate).toBeGreaterThan(0);
218 |       expect(data.averageBuildTime).toBeDefined();
219 |       expect(data.projectCount).toBeGreaterThan(0);
220 |     });
221 | 
222 |     it("should calculate average build time correctly", async () => {
223 |       await createSampleDeployments();
224 | 
225 |       const result = await analyzeDeployments({
226 |         analysisType: "ssg_stats",
227 |         ssg: "hugo",
228 |       });
229 | 
230 |       const content = result.content[0];
231 |       const data = JSON.parse(content.text);
232 | 
233 |       expect(data.averageBuildTime).toBeDefined();
234 |       // Hugo has 3 deployments with build times
235 |       expect(data.averageBuildTime).toBeGreaterThan(0);
236 |       expect(data.averageBuildTime).toBeLessThan(20000);
237 |     });
238 | 
239 |     it("should show success rate less than 100% for failed deployments", async () => {
240 |       await createSampleDeployments();
241 | 
242 |       const result = await analyzeDeployments({
243 |         analysisType: "ssg_stats",
244 |         ssg: "mkdocs",
245 |       });
246 | 
247 |       const content = result.content[0];
248 |       const data = JSON.parse(content.text);
249 | 
250 |       expect(data.totalDeployments).toBeGreaterThanOrEqual(1);
251 |       expect(data.failedDeployments).toBeGreaterThanOrEqual(1);
252 |       expect(data.successRate).toBeLessThan(1.0);
253 |     });
254 |   });
255 | 
256 |   describe("SSG Comparison Analysis", () => {
257 |     it("should fail without enough SSGs", async () => {
258 |       const result = await analyzeDeployments({
259 |         analysisType: "compare",
260 |         ssgs: ["docusaurus"],
261 |       });
262 | 
263 |       const content = result.content[0];
264 |       expect(content.type).toBe("text");
265 |       const data = JSON.parse(content.text);
266 | 
267 |       // Should be an error response
268 |       expect(data.success).toBe(false);
269 |       expect(data.error).toBeDefined();
270 |       expect(data.error.code).toBe("ANALYTICS_FAILED");
271 |     });
272 | 
273 |     it("should compare multiple SSGs by success rate", async () => {
274 |       await createSampleDeployments();
275 | 
276 |       const result = await analyzeDeployments({
277 |         analysisType: "compare",
278 |         ssgs: ["docusaurus", "hugo", "mkdocs"],
279 |       });
280 | 
281 |       const content = result.content[0];
282 |       expect(content.type).toBe("text");
283 |       const data = JSON.parse(content.text);
284 | 
285 |       expect(Array.isArray(data)).toBe(true);
286 |       expect(data.length).toBeGreaterThan(0);
287 | 
288 |       // Should be sorted by success rate (descending)
289 |       for (let i = 0; i < data.length - 1; i++) {
290 |         expect(data[i].pattern.successRate).toBeGreaterThanOrEqual(
291 |           data[i + 1].pattern.successRate,
292 |         );
293 |       }
294 |     });
295 | 
296 |     it("should include only SSGs with deployment data", async () => {
297 |       await createSampleDeployments();
298 | 
299 |       const result = await analyzeDeployments({
300 |         analysisType: "compare",
301 |         ssgs: ["docusaurus", "nonexistent", "hugo"],
302 |       });
303 | 
304 |       const content = result.content[0];
305 |       const data = JSON.parse(content.text);
306 | 
307 |       // Should only include docusaurus and hugo
308 |       expect(data.length).toBe(2);
309 |       const ssgs = data.map((d: any) => d.ssg);
310 |       expect(ssgs).toContain("docusaurus");
311 |       expect(ssgs).toContain("hugo");
312 |       expect(ssgs).not.toContain("nonexistent");
313 |     });
314 |   });
315 | 
316 |   describe("Health Score Analysis", () => {
317 |     it("should calculate health score with no data", async () => {
318 |       const result = await analyzeDeployments({
319 |         analysisType: "health",
320 |       });
321 | 
322 |       const content = result.content[0];
323 |       expect(content.type).toBe("text");
324 |       const data = JSON.parse(content.text);
325 | 
326 |       expect(data.score).toBeDefined();
327 |       expect(data.score).toBeGreaterThanOrEqual(0);
328 |       expect(data.score).toBeLessThanOrEqual(100);
329 |       expect(data.factors).toBeDefined();
330 |       expect(Array.isArray(data.factors)).toBe(true);
331 |       expect(data.factors.length).toBe(4); // 4 factors
332 |     });
333 | 
334 |     it("should calculate health score with sample data", async () => {
335 |       await createSampleDeployments();
336 | 
337 |       const result = await analyzeDeployments({
338 |         analysisType: "health",
339 |       });
340 | 
341 |       const content = result.content[0];
342 |       const data = JSON.parse(content.text);
343 | 
344 |       expect(data.score).toBeGreaterThan(0);
345 |       expect(data.factors.length).toBe(4);
346 | 
347 |       // Check all factors present
348 |       const factorNames = data.factors.map((f: any) => f.name);
349 |       expect(factorNames).toContain("Overall Success Rate");
350 |       expect(factorNames).toContain("Active Projects");
351 |       expect(factorNames).toContain("Deployment Activity");
352 |       expect(factorNames).toContain("SSG Diversity");
353 | 
354 |       // Each factor should have impact and status
355 |       data.factors.forEach((factor: any) => {
356 |         expect(factor.impact).toBeDefined();
357 |         expect(factor.status).toMatch(/^(good|warning|critical)$/);
358 |       });
359 |     });
360 | 
361 |     it("should have good health with high success rate", async () => {
362 |       await createSampleDeployments();
363 | 
364 |       const result = await analyzeDeployments({
365 |         analysisType: "health",
366 |       });
367 | 
368 |       const content = result.content[0];
369 |       const data = JSON.parse(content.text);
370 | 
371 |       // Should have decent health with our sample data
372 |       expect(data.score).toBeGreaterThan(30);
373 | 
374 |       const successRateFactor = data.factors.find(
375 |         (f: any) => f.name === "Overall Success Rate",
376 |       );
377 |       expect(successRateFactor.status).toMatch(/^(good|warning)$/);
378 |     });
379 |   });
380 | 
381 |   describe("Trend Analysis", () => {
382 |     it("should analyze trends with default period", async () => {
383 |       await createSampleDeployments();
384 | 
385 |       const result = await analyzeDeployments({
386 |         analysisType: "trends",
387 |       });
388 | 
389 |       const content = result.content[0];
390 |       expect(content.type).toBe("text");
391 |       const data = JSON.parse(content.text);
392 | 
393 |       expect(Array.isArray(data)).toBe(true);
394 |       // Trends are grouped by time periods
395 |     });
396 | 
397 |     it("should analyze trends with custom period", async () => {
398 |       await createSampleDeployments();
399 | 
400 |       const result = await analyzeDeployments({
401 |         analysisType: "trends",
402 |         periodDays: 7,
403 |       });
404 | 
405 |       const content = result.content[0];
406 |       const data = JSON.parse(content.text);
407 | 
408 |       expect(Array.isArray(data)).toBe(true);
409 |     });
410 |   });
411 | 
412 |   describe("Error Handling", () => {
413 |     it("should handle missing SSG parameter for ssg_stats", async () => {
414 |       const result = await analyzeDeployments({
415 |         analysisType: "ssg_stats",
416 |       });
417 | 
418 |       const content = result.content[0];
419 |       const data = JSON.parse(content.text);
420 | 
421 |       expect(data.success).toBe(false);
422 |       expect(data.error).toBeDefined();
423 |       expect(data.error.code).toBe("ANALYTICS_FAILED");
424 |       expect(data.error.message).toContain("SSG name required");
425 |     });
426 | 
427 |     it("should handle invalid analysis type gracefully", async () => {
428 |       const result = await analyzeDeployments({
429 |         analysisType: "full_report",
430 |       });
431 | 
432 |       const content = result.content[0];
433 |       expect(content.type).toBe("text");
434 |       // Should not throw, should return valid response
435 |     });
436 |   });
437 | 
438 |   describe("Recommendations Generation", () => {
439 |     it("should generate recommendations based on patterns", async () => {
440 |       await createSampleDeployments();
441 | 
442 |       const result = await analyzeDeployments({
443 |         analysisType: "full_report",
444 |       });
445 | 
446 |       const content = result.content[0];
447 |       const data = JSON.parse(content.text);
448 | 
449 |       expect(data.recommendations).toBeDefined();
450 |       expect(Array.isArray(data.recommendations)).toBe(true);
451 |       expect(data.recommendations.length).toBeGreaterThan(0);
452 |     });
453 | 
454 |     it("should recommend best performing SSG", async () => {
455 |       await createSampleDeployments();
456 | 
457 |       const result = await analyzeDeployments({
458 |         analysisType: "full_report",
459 |       });
460 | 
461 |       const content = result.content[0];
462 |       const data = JSON.parse(content.text);
463 | 
464 |       // Should have recommendations
465 |       expect(data.recommendations.length).toBeGreaterThan(0);
466 |       // At least one recommendation should mention an SSG or general advice
467 |       const allText = data.recommendations.join(" ").toLowerCase();
468 |       expect(allText.length).toBeGreaterThan(0);
469 |     });
470 |   });
471 | 
472 |   describe("Build Time Analysis", () => {
473 |     it("should identify fast builds in insights", async () => {
474 |       await createSampleDeployments();
475 | 
476 |       const result = await analyzeDeployments({
477 |         analysisType: "full_report",
478 |       });
479 | 
480 |       const content = result.content[0];
481 |       const data = JSON.parse(content.text);
482 | 
483 |       // Hugo has ~15s builds, should be identified as fast
484 |       const fastBuildInsights = data.insights.filter(
485 |         (i: any) => i.title && i.title.includes("Fast Builds"),
486 |       );
487 |       expect(fastBuildInsights.length).toBeGreaterThan(0);
488 |     });
489 |   });
490 | });
491 | 
```

--------------------------------------------------------------------------------
/tests/memory/kg-code-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for Knowledge Graph Code Integration
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
  6 | import { promises as fs } from "fs";
  7 | import path from "path";
  8 | import { tmpdir } from "os";
  9 | import {
 10 |   createCodeFileEntities,
 11 |   createDocumentationEntities,
 12 |   linkCodeToDocs,
 13 | } from "../../src/memory/kg-code-integration.js";
 14 | import { ExtractedContent } from "../../src/utils/content-extractor.js";
 15 | import {
 16 |   initializeKnowledgeGraph,
 17 |   getKnowledgeGraph,
 18 | } from "../../src/memory/kg-integration.js";
 19 | 
 20 | describe("KG Code Integration", () => {
 21 |   let testDir: string;
 22 |   let projectId: string;
 23 | 
 24 |   beforeEach(async () => {
 25 |     // Create temporary directory for test files
 26 |     testDir = path.join(tmpdir(), `documcp-test-${Date.now()}`);
 27 |     await fs.mkdir(testDir, { recursive: true });
 28 |     projectId = `project:test_${Date.now()}`;
 29 | 
 30 |     // Initialize KG with test storage
 31 |     const storageDir = path.join(testDir, ".documcp/memory");
 32 |     await initializeKnowledgeGraph(storageDir);
 33 |   });
 34 | 
 35 |   afterEach(async () => {
 36 |     // Cleanup
 37 |     try {
 38 |       await fs.rm(testDir, { recursive: true, force: true });
 39 |     } catch {
 40 |       // Ignore cleanup errors
 41 |     }
 42 |   });
 43 | 
 44 |   describe("createCodeFileEntities", () => {
 45 |     it("should create code file entities from TypeScript files", async () => {
 46 |       // Create a test TypeScript file
 47 |       const srcDir = path.join(testDir, "src");
 48 |       await fs.mkdir(srcDir, { recursive: true });
 49 | 
 50 |       const tsContent = `
 51 | export class UserService {
 52 |   async getUser(id: string) {
 53 |     return { id, name: "Test User" };
 54 |   }
 55 | 
 56 |   async createUser(data: any) {
 57 |     return { ...data, id: "123" };
 58 |   }
 59 | }
 60 | 
 61 | export async function validateUser(user: any) {
 62 |   return user.name && user.id;
 63 | }
 64 | `;
 65 | 
 66 |       await fs.writeFile(path.join(srcDir, "user.ts"), tsContent, "utf-8");
 67 | 
 68 |       // Create entities
 69 |       const entities = await createCodeFileEntities(projectId, testDir);
 70 | 
 71 |       // Assertions
 72 |       expect(entities.length).toBe(1);
 73 |       expect(entities[0].type).toBe("code_file");
 74 |       expect(entities[0].properties.language).toBe("typescript");
 75 |       expect(entities[0].properties.path).toBe("src/user.ts");
 76 |       expect(entities[0].properties.classes).toContain("UserService");
 77 |       expect(entities[0].properties.functions).toContain("validateUser");
 78 |       expect(entities[0].properties.contentHash).toBeDefined();
 79 |       expect(entities[0].properties.linesOfCode).toBeGreaterThan(0);
 80 |     });
 81 | 
 82 |     it("should create code file entities from Python files", async () => {
 83 |       const srcDir = path.join(testDir, "src");
 84 |       await fs.mkdir(srcDir, { recursive: true });
 85 | 
 86 |       const pyContent = `
 87 | class Database:
 88 |     def connect(self):
 89 |         pass
 90 | 
 91 |     def query(self, sql):
 92 |         return []
 93 | 
 94 | def initialize_db():
 95 |     return Database()
 96 | `;
 97 | 
 98 |       await fs.writeFile(path.join(srcDir, "database.py"), pyContent, "utf-8");
 99 | 
100 |       const entities = await createCodeFileEntities(projectId, testDir);
101 | 
102 |       expect(entities.length).toBe(1);
103 |       expect(entities[0].properties.language).toBe("python");
104 |       expect(entities[0].properties.classes).toContain("Database");
105 |       expect(entities[0].properties.functions).toContain("initialize_db");
106 |     });
107 | 
108 |     it("should handle nested directories", async () => {
109 |       const nestedDir = path.join(testDir, "src", "services", "auth");
110 |       await fs.mkdir(nestedDir, { recursive: true });
111 | 
112 |       await fs.writeFile(
113 |         path.join(nestedDir, "login.ts"),
114 |         "export function login() {}",
115 |         "utf-8",
116 |       );
117 | 
118 |       const entities = await createCodeFileEntities(projectId, testDir);
119 | 
120 |       expect(entities.length).toBe(1);
121 |       expect(entities[0].properties.path).toBe("src/services/auth/login.ts");
122 |     });
123 | 
124 |     it("should skip non-code files", async () => {
125 |       const srcDir = path.join(testDir, "src");
126 |       await fs.mkdir(srcDir, { recursive: true });
127 | 
128 |       await fs.writeFile(path.join(srcDir, "README.md"), "# Readme", "utf-8");
129 |       await fs.writeFile(path.join(srcDir, "config.json"), "{}", "utf-8");
130 | 
131 |       const entities = await createCodeFileEntities(projectId, testDir);
132 | 
133 |       expect(entities.length).toBe(0);
134 |     });
135 | 
136 |     it("should estimate complexity correctly", async () => {
137 |       const srcDir = path.join(testDir, "src");
138 |       await fs.mkdir(srcDir, { recursive: true });
139 | 
140 |       // Small file - low complexity
141 |       const smallFile = "export function simple() { return 1; }";
142 |       await fs.writeFile(path.join(srcDir, "small.ts"), smallFile, "utf-8");
143 | 
144 |       // Large file - high complexity
145 |       const largeFile = Array(200)
146 |         .fill("function test() { return 1; }")
147 |         .join("\n");
148 |       await fs.writeFile(path.join(srcDir, "large.ts"), largeFile, "utf-8");
149 | 
150 |       const entities = await createCodeFileEntities(projectId, testDir);
151 | 
152 |       const smallEntity = entities.find((e) =>
153 |         e.properties.path.includes("small.ts"),
154 |       );
155 |       const largeEntity = entities.find((e) =>
156 |         e.properties.path.includes("large.ts"),
157 |       );
158 | 
159 |       expect(smallEntity?.properties.complexity).toBe("low");
160 |       expect(largeEntity?.properties.complexity).toBe("high");
161 |     });
162 | 
163 |     it("should create relationships with project", async () => {
164 |       const srcDir = path.join(testDir, "src");
165 |       await fs.mkdir(srcDir, { recursive: true });
166 |       await fs.writeFile(
167 |         path.join(srcDir, "test.ts"),
168 |         "export function test() {}",
169 |         "utf-8",
170 |       );
171 | 
172 |       await createCodeFileEntities(projectId, testDir);
173 | 
174 |       const kg = await getKnowledgeGraph();
175 |       const edges = await kg.findEdges({ source: projectId });
176 | 
177 |       expect(edges.some((e) => e.type === "depends_on")).toBe(true);
178 |     });
179 |   });
180 | 
181 |   describe("createDocumentationEntities", () => {
182 |     it("should create documentation section entities from README", async () => {
183 |       const extractedContent: ExtractedContent = {
184 |         readme: {
185 |           content: "# My Project\n\nThis is a test project.",
186 |           sections: [
187 |             {
188 |               title: "My Project",
189 |               content: "This is a test project.",
190 |               level: 1,
191 |             },
192 |             { title: "Installation", content: "npm install", level: 2 },
193 |           ],
194 |         },
195 |         existingDocs: [],
196 |         adrs: [],
197 |         codeExamples: [],
198 |         apiDocs: [],
199 |       };
200 | 
201 |       const entities = await createDocumentationEntities(
202 |         projectId,
203 |         extractedContent,
204 |       );
205 | 
206 |       expect(entities.length).toBe(2);
207 |       expect(entities[0].type).toBe("documentation_section");
208 |       expect(entities[0].properties.sectionTitle).toBe("My Project");
209 |       expect(entities[0].properties.contentHash).toBeDefined();
210 |       expect(entities[0].properties.category).toBe("reference");
211 |     });
212 | 
213 |     it("should categorize documentation correctly", async () => {
214 |       const extractedContent: ExtractedContent = {
215 |         existingDocs: [
216 |           {
217 |             path: "docs/tutorials/getting-started.md",
218 |             title: "Getting Started",
219 |             content: "# Tutorial",
220 |             category: "tutorial",
221 |           },
222 |           {
223 |             path: "docs/how-to/deploy.md",
224 |             title: "Deploy Guide",
225 |             content: "# How to Deploy",
226 |             category: "how-to",
227 |           },
228 |           {
229 |             path: "docs/api/reference.md",
230 |             title: "API Reference",
231 |             content: "# API",
232 |             category: "reference",
233 |           },
234 |         ],
235 |         adrs: [],
236 |         codeExamples: [],
237 |         apiDocs: [],
238 |       };
239 | 
240 |       const entities = await createDocumentationEntities(
241 |         projectId,
242 |         extractedContent,
243 |       );
244 | 
245 |       expect(entities.length).toBe(3);
246 |       expect(
247 |         entities.find((e) => e.properties.category === "tutorial"),
248 |       ).toBeDefined();
249 |       expect(
250 |         entities.find((e) => e.properties.category === "how-to"),
251 |       ).toBeDefined();
252 |       expect(
253 |         entities.find((e) => e.properties.category === "reference"),
254 |       ).toBeDefined();
255 |     });
256 | 
257 |     it("should extract code references from content", async () => {
258 |       const extractedContent: ExtractedContent = {
259 |         existingDocs: [
260 |           {
261 |             path: "docs/guide.md",
262 |             title: "Guide",
263 |             content:
264 |               "Call `getUserById()` from `src/user.ts` using `UserService` class",
265 |             category: "how-to",
266 |           },
267 |         ],
268 |         adrs: [],
269 |         codeExamples: [],
270 |         apiDocs: [],
271 |       };
272 | 
273 |       const entities = await createDocumentationEntities(
274 |         projectId,
275 |         extractedContent,
276 |       );
277 | 
278 |       expect(entities[0].properties.referencedCodeFiles).toContain(
279 |         "src/user.ts",
280 |       );
281 |       expect(entities[0].properties.referencedFunctions).toContain(
282 |         "getUserById",
283 |       );
284 |       expect(entities[0].properties.referencedClasses).toContain("UserService");
285 |     });
286 | 
287 |     it("should detect code examples in documentation", async () => {
288 |       const extractedContent: ExtractedContent = {
289 |         existingDocs: [
290 |           {
291 |             path: "docs/example.md",
292 |             title: "Example",
293 |             content: "# Example\n\n```typescript\nconst x = 1;\n```",
294 |           },
295 |         ],
296 |         adrs: [],
297 |         codeExamples: [],
298 |         apiDocs: [],
299 |       };
300 | 
301 |       const entities = await createDocumentationEntities(
302 |         projectId,
303 |         extractedContent,
304 |       );
305 | 
306 |       expect(entities[0].properties.hasCodeExamples).toBe(true);
307 |       expect(entities[0].properties.effectivenessScore).toBeGreaterThan(0.5);
308 |     });
309 | 
310 |     it("should process ADRs as explanation category", async () => {
311 |       const extractedContent: ExtractedContent = {
312 |         existingDocs: [],
313 |         adrs: [
314 |           {
315 |             number: "001",
316 |             title: "Use TypeScript",
317 |             status: "Accepted",
318 |             content: "We will use TypeScript for type safety",
319 |             decision: "Use TypeScript",
320 |             consequences: "Better IDE support",
321 |           },
322 |         ],
323 |         codeExamples: [],
324 |         apiDocs: [],
325 |       };
326 | 
327 |       const entities = await createDocumentationEntities(
328 |         projectId,
329 |         extractedContent,
330 |       );
331 | 
332 |       expect(entities.length).toBe(1);
333 |       expect(entities[0].properties.category).toBe("explanation");
334 |       expect(entities[0].properties.sectionTitle).toBe("Use TypeScript");
335 |     });
336 |   });
337 | 
338 |   describe("linkCodeToDocs", () => {
339 |     it("should create references edges when docs reference code", async () => {
340 |       // Create code entity
341 |       const srcDir = path.join(testDir, "src");
342 |       await fs.mkdir(srcDir, { recursive: true });
343 |       await fs.writeFile(
344 |         path.join(srcDir, "user.ts"),
345 |         "export function getUser() {}",
346 |         "utf-8",
347 |       );
348 | 
349 |       const codeFiles = await createCodeFileEntities(projectId, testDir);
350 | 
351 |       // Create doc entity that references the code
352 |       const extractedContent: ExtractedContent = {
353 |         existingDocs: [
354 |           {
355 |             path: "docs/api.md",
356 |             title: "API",
357 |             content: "Use `getUser()` from `src/user.ts`",
358 |             category: "reference",
359 |           },
360 |         ],
361 |         adrs: [],
362 |         codeExamples: [],
363 |         apiDocs: [],
364 |       };
365 | 
366 |       const docSections = await createDocumentationEntities(
367 |         projectId,
368 |         extractedContent,
369 |       );
370 | 
371 |       // Link them
372 |       const edges = await linkCodeToDocs(codeFiles, docSections);
373 | 
374 |       // Should create references edge (doc -> code)
375 |       const referencesEdge = edges.find((e) => e.type === "references");
376 |       expect(referencesEdge).toBeDefined();
377 |       expect(referencesEdge?.source).toBe(docSections[0].id);
378 |       expect(referencesEdge?.target).toBe(codeFiles[0].id);
379 |       expect(referencesEdge?.properties.referenceType).toBe("api-reference");
380 | 
381 |       // Should create documents edge (code -> doc)
382 |       const documentsEdge = edges.find((e) => e.type === "documents");
383 |       expect(documentsEdge).toBeDefined();
384 |       expect(documentsEdge?.source).toBe(codeFiles[0].id);
385 |       expect(documentsEdge?.target).toBe(docSections[0].id);
386 |     });
387 | 
388 |     it("should detect outdated documentation", async () => {
389 |       // Create code entity with recent modification
390 |       const srcDir = path.join(testDir, "src");
391 |       await fs.mkdir(srcDir, { recursive: true });
392 |       await fs.writeFile(
393 |         path.join(srcDir, "user.ts"),
394 |         "export function getUser() {}",
395 |         "utf-8",
396 |       );
397 | 
398 |       const codeFiles = await createCodeFileEntities(projectId, testDir);
399 | 
400 |       // Simulate old documentation (modify lastUpdated)
401 |       const extractedContent: ExtractedContent = {
402 |         existingDocs: [
403 |           {
404 |             path: "docs/api.md",
405 |             title: "API",
406 |             content: "Use `getUser()` from `src/user.ts`",
407 |             category: "reference",
408 |           },
409 |         ],
410 |         adrs: [],
411 |         codeExamples: [],
412 |         apiDocs: [],
413 |       };
414 | 
415 |       const docSections = await createDocumentationEntities(
416 |         projectId,
417 |         extractedContent,
418 |       );
419 | 
420 |       // Manually set old timestamp on doc
421 |       docSections[0].properties.lastUpdated = new Date(
422 |         Date.now() - 86400000,
423 |       ).toISOString();
424 | 
425 |       const edges = await linkCodeToDocs(codeFiles, docSections);
426 | 
427 |       // Should create outdated_for edge
428 |       const outdatedEdge = edges.find((e) => e.type === "outdated_for");
429 |       expect(outdatedEdge).toBeDefined();
430 |       expect(outdatedEdge?.properties.severity).toBe("medium");
431 |     });
432 | 
433 |     it("should determine coverage based on referenced functions", async () => {
434 |       const srcDir = path.join(testDir, "src");
435 |       await fs.mkdir(srcDir, { recursive: true });
436 | 
437 |       // Code with 3 functions
438 |       await fs.writeFile(
439 |         path.join(srcDir, "user.ts"),
440 |         `
441 | export function getUser() {}
442 | export function createUser() {}
443 | export function deleteUser() {}
444 |       `,
445 |         "utf-8",
446 |       );
447 | 
448 |       const codeFiles = await createCodeFileEntities(projectId, testDir);
449 | 
450 |       // Doc that only references 2 functions (66% coverage)
451 |       const extractedContent: ExtractedContent = {
452 |         existingDocs: [
453 |           {
454 |             path: "docs/api.md",
455 |             title: "API",
456 |             content: "Use `getUser()` and `createUser()` from `src/user.ts`",
457 |             category: "reference",
458 |           },
459 |         ],
460 |         adrs: [],
461 |         codeExamples: [],
462 |         apiDocs: [],
463 |       };
464 | 
465 |       const docSections = await createDocumentationEntities(
466 |         projectId,
467 |         extractedContent,
468 |       );
469 | 
470 |       const edges = await linkCodeToDocs(codeFiles, docSections);
471 | 
472 |       const documentsEdge = edges.find((e) => e.type === "documents");
473 |       expect(documentsEdge?.properties.coverage).toBe("complete"); // >= 50%
474 |     });
475 | 
476 |     it("should handle documentation with no code references", async () => {
477 |       const srcDir = path.join(testDir, "src");
478 |       await fs.mkdir(srcDir, { recursive: true });
479 |       await fs.writeFile(
480 |         path.join(srcDir, "user.ts"),
481 |         "export function getUser() {}",
482 |         "utf-8",
483 |       );
484 | 
485 |       const codeFiles = await createCodeFileEntities(projectId, testDir);
486 | 
487 |       // Doc with no code references
488 |       const extractedContent: ExtractedContent = {
489 |         existingDocs: [
490 |           {
491 |             path: "docs/guide.md",
492 |             title: "Guide",
493 |             content: "This is a general guide with no code references",
494 |             category: "tutorial",
495 |           },
496 |         ],
497 |         adrs: [],
498 |         codeExamples: [],
499 |         apiDocs: [],
500 |       };
501 | 
502 |       const docSections = await createDocumentationEntities(
503 |         projectId,
504 |         extractedContent,
505 |       );
506 | 
507 |       const edges = await linkCodeToDocs(codeFiles, docSections);
508 | 
509 |       // Should not create edges between unrelated code and docs
510 |       expect(edges.length).toBe(0);
511 |     });
512 |   });
513 | });
514 | 
```

--------------------------------------------------------------------------------
/tests/tools/recommend-ssg-historical.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for Phase 2.1: Historical Deployment Data Integration
  3 |  * Tests the enhanced recommend_ssg tool with knowledge graph integration
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
  7 | import { promises as fs } from "fs";
  8 | import { join } from "path";
  9 | import { tmpdir } from "os";
 10 | import {
 11 |   initializeKnowledgeGraph,
 12 |   createOrUpdateProject,
 13 |   trackDeployment,
 14 |   getMemoryManager,
 15 | } from "../../src/memory/kg-integration.js";
 16 | import { recommendSSG } from "../../src/tools/recommend-ssg.js";
 17 | import { MemoryManager } from "../../src/memory/manager.js";
 18 | 
 19 | describe("recommendSSG with Historical Data (Phase 2.1)", () => {
 20 |   let testDir: string;
 21 |   let originalEnv: string | undefined;
 22 |   let memoryManager: MemoryManager;
 23 | 
 24 |   beforeEach(async () => {
 25 |     // Create temporary test directory
 26 |     testDir = join(tmpdir(), `recommend-ssg-historical-test-${Date.now()}`);
 27 |     await fs.mkdir(testDir, { recursive: true });
 28 | 
 29 |     // Set environment variable for storage
 30 |     originalEnv = process.env.DOCUMCP_STORAGE_DIR;
 31 |     process.env.DOCUMCP_STORAGE_DIR = testDir;
 32 | 
 33 |     // Initialize KG and memory - this creates the global memory manager
 34 |     await initializeKnowledgeGraph(testDir);
 35 | 
 36 |     // Use the same memory manager instance that kg-integration created
 37 |     memoryManager = await getMemoryManager();
 38 |   });
 39 | 
 40 |   afterEach(async () => {
 41 |     // Restore environment
 42 |     if (originalEnv) {
 43 |       process.env.DOCUMCP_STORAGE_DIR = originalEnv;
 44 |     } else {
 45 |       delete process.env.DOCUMCP_STORAGE_DIR;
 46 |     }
 47 | 
 48 |     // Clean up test directory
 49 |     try {
 50 |       await fs.rm(testDir, { recursive: true, force: true });
 51 |     } catch (error) {
 52 |       console.warn("Failed to clean up test directory:", error);
 53 |     }
 54 |   });
 55 | 
 56 |   describe("Historical Data Retrieval", () => {
 57 |     it("should include historical data when similar projects exist", async () => {
 58 |       // Create a project with successful deployments
 59 |       const project1 = await createOrUpdateProject({
 60 |         id: "test_project_1",
 61 |         timestamp: new Date().toISOString(),
 62 |         path: "/test/project1",
 63 |         projectName: "Test Project 1",
 64 |         structure: {
 65 |           totalFiles: 50,
 66 |           languages: { typescript: 30, javascript: 20 },
 67 |           hasTests: true,
 68 |           hasCI: false,
 69 |           hasDocs: false,
 70 |         },
 71 |       });
 72 | 
 73 |       // Track successful Docusaurus deployments
 74 |       await trackDeployment(project1.id, "docusaurus", true, {
 75 |         buildTime: 45,
 76 |       });
 77 |       await trackDeployment(project1.id, "docusaurus", true, {
 78 |         buildTime: 42,
 79 |       });
 80 | 
 81 |       // Store analysis in memory for recommendation
 82 |       const memoryEntry = await memoryManager.remember("analysis", {
 83 |         path: "/test/project2",
 84 |         dependencies: {
 85 |           ecosystem: "javascript",
 86 |           languages: ["typescript", "javascript"],
 87 |         },
 88 |         structure: { totalFiles: 60 },
 89 |       });
 90 | 
 91 |       // Get recommendation
 92 |       const result = await recommendSSG({
 93 |         analysisId: memoryEntry.id,
 94 |         preferences: {},
 95 |       });
 96 | 
 97 |       const content = result.content[0];
 98 |       expect(content.type).toBe("text");
 99 |       const data = JSON.parse(content.text);
100 | 
101 |       // Should include historical data
102 |       expect(data.historicalData).toBeDefined();
103 |       expect(data.historicalData.similarProjectCount).toBeGreaterThan(0);
104 |       expect(data.historicalData.successRates.docusaurus).toBeDefined();
105 |       expect(data.historicalData.successRates.docusaurus.rate).toBe(1.0);
106 |       expect(data.historicalData.successRates.docusaurus.sampleSize).toBe(2);
107 |     });
108 | 
109 |     it("should boost confidence when historical success rate is high", async () => {
110 |       // Create multiple successful projects
111 |       for (let i = 0; i < 3; i++) {
112 |         const project = await createOrUpdateProject({
113 |           id: `project_${i}`,
114 |           timestamp: new Date().toISOString(),
115 |           path: `/test/project${i}`,
116 |           projectName: `Project ${i}`,
117 |           structure: {
118 |             totalFiles: 50,
119 |             languages: { typescript: 50 },
120 |             hasTests: true,
121 |             hasCI: false,
122 |             hasDocs: false,
123 |           },
124 |         });
125 | 
126 |         // Track successful Hugo deployments
127 |         await trackDeployment(project.id, "hugo", true, { buildTime: 30 });
128 |       }
129 | 
130 |       // Store analysis
131 |       const memoryEntry = await memoryManager.remember("analysis", {
132 |         path: "/test/new-project",
133 |         dependencies: {
134 |           ecosystem: "go",
135 |           languages: ["typescript"],
136 |         },
137 |         structure: { totalFiles: 60 },
138 |       });
139 | 
140 |       const result = await recommendSSG({ analysisId: memoryEntry.id });
141 |       const content = result.content[0];
142 |       const data = JSON.parse(content.text);
143 | 
144 |       // Should have high confidence due to historical success
145 |       expect(data.confidence).toBeGreaterThan(0.9);
146 |       expect(data.reasoning[0]).toContain("100% success rate");
147 |     });
148 | 
149 |     it("should reduce confidence when historical success rate is low", async () => {
150 |       // Create project with failed deployments
151 |       const project = await createOrUpdateProject({
152 |         id: "failing_project",
153 |         timestamp: new Date().toISOString(),
154 |         path: "/test/failing",
155 |         projectName: "Failing Project",
156 |         structure: {
157 |           totalFiles: 50,
158 |           languages: { python: 50 },
159 |           hasTests: true,
160 |           hasCI: false,
161 |           hasDocs: false,
162 |         },
163 |       });
164 | 
165 |       // Track mostly failed Jekyll deployments
166 |       await trackDeployment(project.id, "jekyll", false, {
167 |         errorMessage: "Build failed",
168 |       });
169 |       await trackDeployment(project.id, "jekyll", false, {
170 |         errorMessage: "Build failed",
171 |       });
172 |       await trackDeployment(project.id, "jekyll", true, { buildTime: 60 });
173 | 
174 |       // Store analysis
175 |       const memoryEntry003 = await memoryManager.remember("analysis", {
176 |         path: "/test/new-python",
177 |         dependencies: {
178 |           ecosystem: "python",
179 |           languages: ["python"],
180 |         },
181 |         structure: { totalFiles: 60 },
182 |       });
183 | 
184 |       const result = await recommendSSG({ analysisId: memoryEntry003.id });
185 |       const content = result.content[0];
186 |       const data = JSON.parse(content.text);
187 | 
188 |       // Should have reduced confidence
189 |       expect(data.confidence).toBeLessThan(0.8);
190 |       expect(data.reasoning[0]).toContain("33% success rate");
191 |     });
192 | 
193 |     it("should switch to top performer when significantly better", async () => {
194 |       // Create projects with mixed results
195 |       const project1 = await createOrUpdateProject({
196 |         id: "project_mixed_1",
197 |         timestamp: new Date().toISOString(),
198 |         path: "/test/mixed1",
199 |         projectName: "Mixed Project 1",
200 |         structure: {
201 |           totalFiles: 50,
202 |           languages: { javascript: 50 },
203 |           hasTests: true,
204 |           hasCI: false,
205 |           hasDocs: false,
206 |         },
207 |       });
208 | 
209 |       // Docusaurus: 50% success rate (2 samples)
210 |       await trackDeployment(project1.id, "docusaurus", true);
211 |       await trackDeployment(project1.id, "docusaurus", false);
212 | 
213 |       // Eleventy: 100% success rate (3 samples)
214 |       const project2 = await createOrUpdateProject({
215 |         id: "project_mixed_2",
216 |         timestamp: new Date().toISOString(),
217 |         path: "/test/mixed2",
218 |         projectName: "Mixed Project 2",
219 |         structure: {
220 |           totalFiles: 50,
221 |           languages: { javascript: 50 },
222 |           hasTests: true,
223 |           hasCI: false,
224 |           hasDocs: false,
225 |         },
226 |       });
227 | 
228 |       await trackDeployment(project2.id, "eleventy", true);
229 |       await trackDeployment(project2.id, "eleventy", true);
230 |       await trackDeployment(project2.id, "eleventy", true);
231 | 
232 |       // Store analysis preferring JavaScript
233 |       const memoryEntry004 = await memoryManager.remember("analysis", {
234 |         path: "/test/new-js",
235 |         dependencies: {
236 |           ecosystem: "javascript",
237 |           languages: ["javascript"],
238 |         },
239 |         structure: { totalFiles: 40 },
240 |       });
241 | 
242 |       const result = await recommendSSG({ analysisId: memoryEntry004.id });
243 |       const content = result.content[0];
244 |       const data = JSON.parse(content.text);
245 | 
246 |       // Should switch to Eleventy due to better success rate
247 |       expect(data.recommended).toBe("eleventy");
248 |       expect(data.reasoning[0]).toContain("Switching to eleventy");
249 |       expect(data.reasoning[0]).toContain("100% success rate");
250 |     });
251 | 
252 |     it("should mention top performer as alternative if not switching", async () => {
253 |       // Create successful Hugo deployments
254 |       const project = await createOrUpdateProject({
255 |         id: "hugo_success",
256 |         timestamp: new Date().toISOString(),
257 |         path: "/test/hugo",
258 |         projectName: "Hugo Success",
259 |         structure: {
260 |           totalFiles: 100,
261 |           languages: { go: 80, markdown: 20 },
262 |           hasTests: true,
263 |           hasCI: false,
264 |           hasDocs: false,
265 |         },
266 |       });
267 | 
268 |       await trackDeployment(project.id, "hugo", true);
269 |       await trackDeployment(project.id, "hugo", true);
270 | 
271 |       // Store analysis for different ecosystem
272 |       const memoryEntry005 = await memoryManager.remember("analysis", {
273 |         path: "/test/new-python",
274 |         dependencies: {
275 |           ecosystem: "python",
276 |           languages: ["python"],
277 |         },
278 |         structure: { totalFiles: 60 },
279 |       });
280 | 
281 |       const result = await recommendSSG({ analysisId: memoryEntry005.id });
282 |       const content = result.content[0];
283 |       const data = JSON.parse(content.text);
284 | 
285 |       // Should keep Python recommendation but mention Hugo
286 |       expect(data.recommended).toBe("mkdocs");
287 |       const hugoMention = data.reasoning.find((r: string) =>
288 |         r.includes("hugo"),
289 |       );
290 |       expect(hugoMention).toBeDefined();
291 |     });
292 | 
293 |     it("should include deployment statistics in reasoning", async () => {
294 |       // Create multiple projects with various deployments
295 |       for (let i = 0; i < 3; i++) {
296 |         const project = await createOrUpdateProject({
297 |           id: `stats_project_${i}`,
298 |           timestamp: new Date().toISOString(),
299 |           path: `/test/stats${i}`,
300 |           projectName: `Stats Project ${i}`,
301 |           structure: {
302 |             totalFiles: 50,
303 |             languages: { typescript: 50 },
304 |             hasTests: true,
305 |             hasCI: false,
306 |             hasDocs: false,
307 |           },
308 |         });
309 | 
310 |         await trackDeployment(project.id, "docusaurus", true);
311 |         await trackDeployment(project.id, "docusaurus", true);
312 |       }
313 | 
314 |       const memoryEntry006 = await memoryManager.remember("analysis", {
315 |         path: "/test/stats-new",
316 |         dependencies: {
317 |           ecosystem: "javascript",
318 |           languages: ["typescript"],
319 |         },
320 |         structure: { totalFiles: 50 },
321 |       });
322 | 
323 |       const result = await recommendSSG({ analysisId: memoryEntry006.id });
324 |       const content = result.content[0];
325 |       const data = JSON.parse(content.text);
326 | 
327 |       // Should mention deployment statistics
328 |       const statsReasoning = data.reasoning.find((r: string) =>
329 |         r.includes("deployment(s) across"),
330 |       );
331 |       expect(statsReasoning).toBeDefined();
332 |       expect(statsReasoning).toContain("6 deployment(s)");
333 |       expect(statsReasoning).toContain("3 similar project(s)");
334 |     });
335 |   });
336 | 
337 |   describe("Historical Data Structure", () => {
338 |     it("should provide complete historical data structure", async () => {
339 |       const project = await createOrUpdateProject({
340 |         id: "structure_test",
341 |         timestamp: new Date().toISOString(),
342 |         path: "/test/structure",
343 |         projectName: "Structure Test",
344 |         structure: {
345 |           totalFiles: 50,
346 |           languages: { javascript: 50 },
347 |           hasTests: true,
348 |           hasCI: false,
349 |           hasDocs: false,
350 |         },
351 |       });
352 | 
353 |       await trackDeployment(project.id, "jekyll", true);
354 |       await trackDeployment(project.id, "hugo", true);
355 |       await trackDeployment(project.id, "hugo", true);
356 | 
357 |       const memoryEntry007 = await memoryManager.remember("analysis", {
358 |         path: "/test/structure-new",
359 |         dependencies: {
360 |           ecosystem: "javascript",
361 |           languages: ["javascript"],
362 |         },
363 |         structure: { totalFiles: 50 },
364 |       });
365 | 
366 |       const result = await recommendSSG({ analysisId: memoryEntry007.id });
367 |       const content = result.content[0];
368 |       const data = JSON.parse(content.text);
369 | 
370 |       expect(data.historicalData).toBeDefined();
371 |       expect(data.historicalData.similarProjectCount).toBe(1);
372 |       expect(data.historicalData.successRates).toBeDefined();
373 |       expect(data.historicalData.successRates.jekyll).toEqual({
374 |         rate: 1.0,
375 |         sampleSize: 1,
376 |       });
377 |       expect(data.historicalData.successRates.hugo).toEqual({
378 |         rate: 1.0,
379 |         sampleSize: 2,
380 |       });
381 |       expect(data.historicalData.topPerformer).toBeDefined();
382 |       expect(data.historicalData.topPerformer?.ssg).toBe("hugo");
383 |       expect(data.historicalData.topPerformer?.deploymentCount).toBe(2);
384 |     });
385 | 
386 |     it("should handle no historical data gracefully", async () => {
387 |       const memoryEntry008 = await memoryManager.remember("analysis", {
388 |         path: "/test/no-history",
389 |         dependencies: {
390 |           ecosystem: "ruby",
391 |           languages: ["ruby"],
392 |         },
393 |         structure: { totalFiles: 30 },
394 |       });
395 | 
396 |       const result = await recommendSSG({ analysisId: memoryEntry008.id });
397 |       const content = result.content[0];
398 |       const data = JSON.parse(content.text);
399 | 
400 |       // Should still make recommendation
401 |       expect(data.recommended).toBe("jekyll");
402 |       expect(data.confidence).toBeGreaterThan(0);
403 | 
404 |       // Historical data should show no similar projects
405 |       expect(data.historicalData).toBeDefined();
406 |       expect(data.historicalData.similarProjectCount).toBe(0);
407 |       expect(Object.keys(data.historicalData.successRates)).toHaveLength(0);
408 |     });
409 |   });
410 | 
411 |   describe("Edge Cases", () => {
412 |     it("should handle single deployment samples cautiously", async () => {
413 |       const project = await createOrUpdateProject({
414 |         id: "single_sample",
415 |         timestamp: new Date().toISOString(),
416 |         path: "/test/single",
417 |         projectName: "Single Sample",
418 |         structure: {
419 |           totalFiles: 50,
420 |           languages: { python: 50 },
421 |           hasTests: true,
422 |           hasCI: false,
423 |           hasDocs: false,
424 |         },
425 |       });
426 | 
427 |       // Single successful deployment
428 |       await trackDeployment(project.id, "mkdocs", true);
429 | 
430 |       const memoryEntry009 = await memoryManager.remember("analysis", {
431 |         path: "/test/single-new",
432 |         dependencies: {
433 |           ecosystem: "python",
434 |           languages: ["python"],
435 |         },
436 |         structure: { totalFiles: 50 },
437 |       });
438 | 
439 |       const result = await recommendSSG({ analysisId: memoryEntry009.id });
440 |       const content = result.content[0];
441 |       const data = JSON.parse(content.text);
442 | 
443 |       // Should not be a top performer with only 1 sample
444 |       expect(data.historicalData?.topPerformer).toBeUndefined();
445 |     });
446 | 
447 |     it("should handle knowledge graph initialization failure", async () => {
448 |       // Use invalid storage directory
449 |       const invalidDir = "/invalid/path/that/does/not/exist";
450 |       const memoryEntry010 = await memoryManager.remember("analysis", {
451 |         path: "/test/kg-fail",
452 |         dependencies: {
453 |           ecosystem: "javascript",
454 |           languages: ["javascript"],
455 |         },
456 |         structure: { totalFiles: 50 },
457 |       });
458 | 
459 |       // Should still make recommendation despite KG failure
460 |       const result = await recommendSSG({ analysisId: memoryEntry010.id });
461 |       const content = result.content[0];
462 |       const data = JSON.parse(content.text);
463 | 
464 |       expect(data.recommended).toBeDefined();
465 |       expect(data.confidence).toBeGreaterThan(0);
466 |     });
467 |   });
468 | });
469 | 
```

--------------------------------------------------------------------------------
/src/memory/deployment-analytics.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Deployment Analytics Module
  3 |  * Phase 2.4: Pattern Analysis and Insights
  4 |  *
  5 |  * Analyzes deployment history to identify patterns, trends, and provide insights
  6 |  */
  7 | 
  8 | import { getKnowledgeGraph } from "./kg-integration.js";
  9 | import { GraphNode, GraphEdge } from "./knowledge-graph.js";
 10 | 
 11 | export interface DeploymentPattern {
 12 |   ssg: string;
 13 |   totalDeployments: number;
 14 |   successfulDeployments: number;
 15 |   failedDeployments: number;
 16 |   successRate: number;
 17 |   averageBuildTime?: number;
 18 |   commonTechnologies: string[];
 19 |   projectCount: number;
 20 | }
 21 | 
 22 | export interface DeploymentTrend {
 23 |   period: string;
 24 |   deployments: number;
 25 |   successRate: number;
 26 |   topSSG: string;
 27 | }
 28 | 
 29 | export interface DeploymentInsight {
 30 |   type: "success" | "warning" | "recommendation";
 31 |   title: string;
 32 |   description: string;
 33 |   ssg?: string;
 34 |   metric?: number;
 35 | }
 36 | 
 37 | export interface AnalyticsReport {
 38 |   summary: {
 39 |     totalProjects: number;
 40 |     totalDeployments: number;
 41 |     overallSuccessRate: number;
 42 |     mostUsedSSG: string;
 43 |     mostSuccessfulSSG: string;
 44 |   };
 45 |   patterns: DeploymentPattern[];
 46 |   insights: DeploymentInsight[];
 47 |   recommendations: string[];
 48 | }
 49 | 
 50 | /**
 51 |  * Deployment Analytics Engine
 52 |  */
 53 | export class DeploymentAnalytics {
 54 |   /**
 55 |    * Generate comprehensive analytics report
 56 |    */
 57 |   async generateReport(): Promise<AnalyticsReport> {
 58 |     const kg = await getKnowledgeGraph();
 59 | 
 60 |     // Get all projects and deployments
 61 |     const projects = await kg.findNodes({ type: "project" });
 62 |     const deploymentEdges = await kg.findEdges({
 63 |       properties: { baseType: "project_deployed_with" },
 64 |     });
 65 | 
 66 |     // Aggregate deployment data by SSG
 67 |     const ssgStats = await this.aggregateSSGStatistics(
 68 |       projects,
 69 |       deploymentEdges,
 70 |     );
 71 | 
 72 |     // Calculate summary metrics
 73 |     const summary = this.calculateSummary(ssgStats, projects.length);
 74 | 
 75 |     // Identify patterns
 76 |     const patterns = this.identifyPatterns(ssgStats);
 77 | 
 78 |     // Generate insights
 79 |     const insights = this.generateInsights(patterns, summary);
 80 | 
 81 |     // Generate recommendations
 82 |     const recommendations = this.generateRecommendations(patterns, insights);
 83 | 
 84 |     return {
 85 |       summary,
 86 |       patterns,
 87 |       insights,
 88 |       recommendations,
 89 |     };
 90 |   }
 91 | 
 92 |   /**
 93 |    * Get deployment statistics for a specific SSG
 94 |    */
 95 |   async getSSGStatistics(ssg: string): Promise<DeploymentPattern | null> {
 96 |     const kg = await getKnowledgeGraph();
 97 | 
 98 |     const deployments = await kg.findEdges({
 99 |       properties: { baseType: "project_deployed_with" },
100 |     });
101 | 
102 |     const allNodes = await kg.getAllNodes();
103 | 
104 |     // Filter deployments for this SSG
105 |     const ssgDeployments = deployments.filter((edge) => {
106 |       const configNode = allNodes.find((n) => n.id === edge.target);
107 |       return configNode?.properties.ssg === ssg;
108 |     });
109 | 
110 |     if (ssgDeployments.length === 0) {
111 |       return null;
112 |     }
113 | 
114 |     const successful = ssgDeployments.filter(
115 |       (d) => d.properties.success,
116 |     ).length;
117 |     const failed = ssgDeployments.length - successful;
118 | 
119 |     // Calculate average build time
120 |     const buildTimes = ssgDeployments
121 |       .filter((d) => d.properties.buildTime)
122 |       .map((d) => d.properties.buildTime as number);
123 | 
124 |     const averageBuildTime =
125 |       buildTimes.length > 0
126 |         ? buildTimes.reduce((a, b) => a + b, 0) / buildTimes.length
127 |         : undefined;
128 | 
129 |     // Get unique projects using this SSG
130 |     const projectIds = new Set(ssgDeployments.map((d) => d.source));
131 | 
132 |     // Get common technologies from projects
133 |     const technologies = new Set<string>();
134 |     for (const projectId of projectIds) {
135 |       const project = allNodes.find((n) => n.id === projectId);
136 |       if (project?.properties.technologies) {
137 |         project.properties.technologies.forEach((tech: string) =>
138 |           technologies.add(tech),
139 |         );
140 |       }
141 |     }
142 | 
143 |     return {
144 |       ssg,
145 |       totalDeployments: ssgDeployments.length,
146 |       successfulDeployments: successful,
147 |       failedDeployments: failed,
148 |       successRate: successful / ssgDeployments.length,
149 |       averageBuildTime,
150 |       commonTechnologies: Array.from(technologies),
151 |       projectCount: projectIds.size,
152 |     };
153 |   }
154 | 
155 |   /**
156 |    * Compare multiple SSGs
157 |    */
158 |   async compareSSGs(
159 |     ssgs: string[],
160 |   ): Promise<{ ssg: string; pattern: DeploymentPattern }[]> {
161 |     const comparisons: { ssg: string; pattern: DeploymentPattern }[] = [];
162 | 
163 |     for (const ssg of ssgs) {
164 |       const pattern = await this.getSSGStatistics(ssg);
165 |       if (pattern) {
166 |         comparisons.push({ ssg, pattern });
167 |       }
168 |     }
169 | 
170 |     // Sort by success rate
171 |     return comparisons.sort(
172 |       (a, b) => b.pattern.successRate - a.pattern.successRate,
173 |     );
174 |   }
175 | 
176 |   /**
177 |    * Identify deployment trends over time
178 |    */
179 |   async identifyTrends(periodDays: number = 30): Promise<DeploymentTrend[]> {
180 |     const kg = await getKnowledgeGraph();
181 |     const deployments = await kg.findEdges({
182 |       properties: { baseType: "project_deployed_with" },
183 |     });
184 | 
185 |     // Group deployments by time period
186 |     const now = Date.now();
187 |     const periodMs = periodDays * 24 * 60 * 60 * 1000;
188 | 
189 |     const trends: Map<string, DeploymentTrend> = new Map();
190 | 
191 |     for (const deployment of deployments) {
192 |       const timestamp = deployment.properties.timestamp;
193 |       if (!timestamp) continue;
194 | 
195 |       const deploymentTime = new Date(timestamp).getTime();
196 |       const periodsAgo = Math.floor((now - deploymentTime) / periodMs);
197 | 
198 |       if (periodsAgo < 0 || periodsAgo > 12) continue; // Last 12 periods
199 | 
200 |       const periodKey = `${periodsAgo} periods ago`;
201 | 
202 |       if (!trends.has(periodKey)) {
203 |         trends.set(periodKey, {
204 |           period: periodKey,
205 |           deployments: 0,
206 |           successRate: 0,
207 |           topSSG: "",
208 |         });
209 |       }
210 | 
211 |       const trend = trends.get(periodKey)!;
212 |       trend.deployments++;
213 | 
214 |       if (deployment.properties.success) {
215 |         trend.successRate++;
216 |       }
217 |     }
218 | 
219 |     // Calculate success rates and identify top SSG per period
220 |     for (const trend of trends.values()) {
221 |       trend.successRate = trend.successRate / trend.deployments;
222 |     }
223 | 
224 |     return Array.from(trends.values()).sort((a, b) =>
225 |       a.period.localeCompare(b.period),
226 |     );
227 |   }
228 | 
229 |   /**
230 |    * Get deployment health score (0-100)
231 |    */
232 |   async getHealthScore(): Promise<{
233 |     score: number;
234 |     factors: {
235 |       name: string;
236 |       impact: number;
237 |       status: "good" | "warning" | "critical";
238 |     }[];
239 |   }> {
240 |     const report = await this.generateReport();
241 | 
242 |     const factors: {
243 |       name: string;
244 |       impact: number;
245 |       status: "good" | "warning" | "critical";
246 |     }[] = [];
247 |     let totalScore = 0;
248 | 
249 |     // Factor 1: Overall success rate (40 points)
250 |     const successRateScore = report.summary.overallSuccessRate * 40;
251 |     totalScore += successRateScore;
252 |     factors.push({
253 |       name: "Overall Success Rate",
254 |       impact: successRateScore,
255 |       status:
256 |         report.summary.overallSuccessRate > 0.8
257 |           ? "good"
258 |           : report.summary.overallSuccessRate > 0.5
259 |             ? "warning"
260 |             : "critical",
261 |     });
262 | 
263 |     // Factor 2: Number of projects (20 points)
264 |     const projectScore = Math.min(20, report.summary.totalProjects * 2);
265 |     totalScore += projectScore;
266 |     factors.push({
267 |       name: "Active Projects",
268 |       impact: projectScore,
269 |       status:
270 |         report.summary.totalProjects > 5
271 |           ? "good"
272 |           : report.summary.totalProjects > 2
273 |             ? "warning"
274 |             : "critical",
275 |     });
276 | 
277 |     // Factor 3: Deployment frequency (20 points)
278 |     const deploymentScore = Math.min(20, report.summary.totalDeployments * 1.5);
279 |     totalScore += deploymentScore;
280 |     factors.push({
281 |       name: "Deployment Activity",
282 |       impact: deploymentScore,
283 |       status:
284 |         report.summary.totalDeployments > 10
285 |           ? "good"
286 |           : report.summary.totalDeployments > 5
287 |             ? "warning"
288 |             : "critical",
289 |     });
290 | 
291 |     // Factor 4: SSG diversity (20 points)
292 |     const ssgDiversity = report.patterns.length;
293 |     const diversityScore = Math.min(20, ssgDiversity * 5);
294 |     totalScore += diversityScore;
295 |     factors.push({
296 |       name: "SSG Diversity",
297 |       impact: diversityScore,
298 |       status:
299 |         ssgDiversity > 3 ? "good" : ssgDiversity > 1 ? "warning" : "critical",
300 |     });
301 | 
302 |     return {
303 |       score: Math.round(totalScore),
304 |       factors,
305 |     };
306 |   }
307 | 
308 |   /**
309 |    * Private: Aggregate SSG statistics
310 |    */
311 |   private async aggregateSSGStatistics(
312 |     projects: GraphNode[],
313 |     deploymentEdges: GraphEdge[],
314 |   ): Promise<Map<string, DeploymentPattern>> {
315 |     const kg = await getKnowledgeGraph();
316 |     const allNodes = await kg.getAllNodes();
317 |     const ssgStats = new Map<string, DeploymentPattern>();
318 | 
319 |     for (const deployment of deploymentEdges) {
320 |       const configNode = allNodes.find((n) => n.id === deployment.target);
321 |       if (!configNode || configNode.type !== "configuration") continue;
322 | 
323 |       const ssg = configNode.properties.ssg;
324 |       if (!ssg) continue;
325 | 
326 |       if (!ssgStats.has(ssg)) {
327 |         ssgStats.set(ssg, {
328 |           ssg,
329 |           totalDeployments: 0,
330 |           successfulDeployments: 0,
331 |           failedDeployments: 0,
332 |           successRate: 0,
333 |           commonTechnologies: [],
334 |           projectCount: 0,
335 |         });
336 |       }
337 | 
338 |       const stats = ssgStats.get(ssg)!;
339 |       stats.totalDeployments++;
340 | 
341 |       if (deployment.properties.success) {
342 |         stats.successfulDeployments++;
343 |       } else {
344 |         stats.failedDeployments++;
345 |       }
346 | 
347 |       // Track build times
348 |       if (deployment.properties.buildTime) {
349 |         if (!stats.averageBuildTime) {
350 |           stats.averageBuildTime = 0;
351 |         }
352 |         stats.averageBuildTime += deployment.properties.buildTime;
353 |       }
354 |     }
355 | 
356 |     // Calculate final metrics
357 |     for (const stats of ssgStats.values()) {
358 |       stats.successRate = stats.successfulDeployments / stats.totalDeployments;
359 |       if (stats.averageBuildTime) {
360 |         stats.averageBuildTime /= stats.totalDeployments;
361 |       }
362 |     }
363 | 
364 |     return ssgStats;
365 |   }
366 | 
367 |   /**
368 |    * Private: Calculate summary metrics
369 |    */
370 |   private calculateSummary(
371 |     ssgStats: Map<string, DeploymentPattern>,
372 |     projectCount: number,
373 |   ): AnalyticsReport["summary"] {
374 |     let totalDeployments = 0;
375 |     let totalSuccessful = 0;
376 |     let mostUsedSSG = "";
377 |     let mostUsedCount = 0;
378 |     let mostSuccessfulSSG = "";
379 |     let highestSuccessRate = 0;
380 | 
381 |     for (const [ssg, stats] of ssgStats.entries()) {
382 |       totalDeployments += stats.totalDeployments;
383 |       totalSuccessful += stats.successfulDeployments;
384 | 
385 |       if (stats.totalDeployments > mostUsedCount) {
386 |         mostUsedCount = stats.totalDeployments;
387 |         mostUsedSSG = ssg;
388 |       }
389 | 
390 |       if (
391 |         stats.successRate > highestSuccessRate &&
392 |         stats.totalDeployments >= 2
393 |       ) {
394 |         highestSuccessRate = stats.successRate;
395 |         mostSuccessfulSSG = ssg;
396 |       }
397 |     }
398 | 
399 |     return {
400 |       totalProjects: projectCount,
401 |       totalDeployments,
402 |       overallSuccessRate:
403 |         totalDeployments > 0 ? totalSuccessful / totalDeployments : 0,
404 |       mostUsedSSG: mostUsedSSG || "none",
405 |       mostSuccessfulSSG: mostSuccessfulSSG || mostUsedSSG || "none",
406 |     };
407 |   }
408 | 
409 |   /**
410 |    * Private: Identify patterns
411 |    */
412 |   private identifyPatterns(
413 |     ssgStats: Map<string, DeploymentPattern>,
414 |   ): DeploymentPattern[] {
415 |     return Array.from(ssgStats.values()).sort(
416 |       (a, b) => b.totalDeployments - a.totalDeployments,
417 |     );
418 |   }
419 | 
420 |   /**
421 |    * Private: Generate insights
422 |    */
423 |   private generateInsights(
424 |     patterns: DeploymentPattern[],
425 |     summary: AnalyticsReport["summary"],
426 |   ): DeploymentInsight[] {
427 |     const insights: DeploymentInsight[] = [];
428 | 
429 |     // Overall health insight
430 |     if (summary.overallSuccessRate > 0.8) {
431 |       insights.push({
432 |         type: "success",
433 |         title: "High Success Rate",
434 |         description: `Excellent! ${(summary.overallSuccessRate * 100).toFixed(
435 |           1,
436 |         )}% of deployments succeed`,
437 |         metric: summary.overallSuccessRate,
438 |       });
439 |     } else if (summary.overallSuccessRate < 0.5) {
440 |       insights.push({
441 |         type: "warning",
442 |         title: "Low Success Rate",
443 |         description: `Only ${(summary.overallSuccessRate * 100).toFixed(
444 |           1,
445 |         )}% of deployments succeed. Review common failure patterns.`,
446 |         metric: summary.overallSuccessRate,
447 |       });
448 |     }
449 | 
450 |     // SSG-specific insights
451 |     for (const pattern of patterns) {
452 |       if (pattern.successRate === 1.0 && pattern.totalDeployments >= 3) {
453 |         insights.push({
454 |           type: "success",
455 |           title: `${pattern.ssg} Perfect Track Record`,
456 |           description: `All ${pattern.totalDeployments} deployments with ${pattern.ssg} succeeded`,
457 |           ssg: pattern.ssg,
458 |           metric: pattern.successRate,
459 |         });
460 |       } else if (pattern.successRate < 0.5 && pattern.totalDeployments >= 2) {
461 |         insights.push({
462 |           type: "warning",
463 |           title: `${pattern.ssg} Struggling`,
464 |           description: `Only ${(pattern.successRate * 100).toFixed(
465 |             0,
466 |           )}% success rate with ${pattern.ssg}`,
467 |           ssg: pattern.ssg,
468 |           metric: pattern.successRate,
469 |         });
470 |       }
471 | 
472 |       // Build time insights
473 |       if (pattern.averageBuildTime) {
474 |         if (pattern.averageBuildTime < 30000) {
475 |           insights.push({
476 |             type: "success",
477 |             title: `${pattern.ssg} Fast Builds`,
478 |             description: `Average build time: ${(
479 |               pattern.averageBuildTime / 1000
480 |             ).toFixed(1)}s`,
481 |             ssg: pattern.ssg,
482 |             metric: pattern.averageBuildTime,
483 |           });
484 |         } else if (pattern.averageBuildTime > 120000) {
485 |           insights.push({
486 |             type: "warning",
487 |             title: `${pattern.ssg} Slow Builds`,
488 |             description: `Average build time: ${(
489 |               pattern.averageBuildTime / 1000
490 |             ).toFixed(1)}s. Consider optimization.`,
491 |             ssg: pattern.ssg,
492 |             metric: pattern.averageBuildTime,
493 |           });
494 |         }
495 |       }
496 |     }
497 | 
498 |     return insights;
499 |   }
500 | 
501 |   /**
502 |    * Private: Generate recommendations
503 |    */
504 |   private generateRecommendations(
505 |     patterns: DeploymentPattern[],
506 |     insights: DeploymentInsight[],
507 |   ): string[] {
508 |     const recommendations: string[] = [];
509 | 
510 |     // Find best performing SSG
511 |     const bestSSG = patterns.find(
512 |       (p) => p.successRate > 0.8 && p.totalDeployments >= 2,
513 |     );
514 |     if (bestSSG) {
515 |       recommendations.push(
516 |         `Consider using ${bestSSG.ssg} for new projects (${(
517 |           bestSSG.successRate * 100
518 |         ).toFixed(0)}% success rate)`,
519 |       );
520 |     }
521 | 
522 |     // Identify problematic SSGs
523 |     const problematicSSG = patterns.find(
524 |       (p) => p.successRate < 0.5 && p.totalDeployments >= 3,
525 |     );
526 |     if (problematicSSG) {
527 |       recommendations.push(
528 |         `Review ${problematicSSG.ssg} deployment process - ${problematicSSG.failedDeployments} recent failures`,
529 |       );
530 |     }
531 | 
532 |     // Diversity recommendation
533 |     if (patterns.length < 2) {
534 |       recommendations.push(
535 |         "Experiment with different SSGs to find the best fit for different project types",
536 |       );
537 |     }
538 | 
539 |     // Activity recommendation
540 |     const totalDeployments = patterns.reduce(
541 |       (sum, p) => sum + p.totalDeployments,
542 |       0,
543 |     );
544 |     if (totalDeployments < 5) {
545 |       recommendations.push(
546 |         "Deploy more projects to build a robust historical dataset for better recommendations",
547 |       );
548 |     }
549 | 
550 |     // Warning-based recommendations
551 |     const warnings = insights.filter((i) => i.type === "warning");
552 |     if (warnings.length > 2) {
553 |       recommendations.push(
554 |         "Multiple deployment issues detected - consider reviewing documentation setup process",
555 |       );
556 |     }
557 | 
558 |     return recommendations;
559 |   }
560 | }
561 | 
562 | /**
563 |  * Get singleton analytics instance
564 |  */
565 | let analyticsInstance: DeploymentAnalytics | null = null;
566 | 
567 | export function getDeploymentAnalytics(): DeploymentAnalytics {
568 |   if (!analyticsInstance) {
569 |     analyticsInstance = new DeploymentAnalytics();
570 |   }
571 |   return analyticsInstance;
572 | }
573 | 
```

--------------------------------------------------------------------------------
/tests/tools/generate-readme-template.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
  2 | import { promises as fs } from "fs";
  3 | import * as path from "path";
  4 | import * as tmp from "tmp";
  5 | import {
  6 |   generateReadmeTemplate,
  7 |   ReadmeTemplateGenerator,
  8 |   GenerateReadmeTemplateSchema,
  9 |   TemplateType,
 10 | } from "../../src/tools/generate-readme-template";
 11 | 
 12 | describe("README Template Generator", () => {
 13 |   let tempDir: string;
 14 |   let generator: ReadmeTemplateGenerator;
 15 | 
 16 |   beforeEach(() => {
 17 |     tempDir = tmp.dirSync({ unsafeCleanup: true }).name;
 18 |     generator = new ReadmeTemplateGenerator();
 19 |   });
 20 | 
 21 |   afterEach(async () => {
 22 |     try {
 23 |       await fs.rmdir(tempDir, { recursive: true });
 24 |     } catch {
 25 |       // Ignore cleanup errors
 26 |     }
 27 |   });
 28 | 
 29 |   describe("Input Validation", () => {
 30 |     it("should validate required fields", () => {
 31 |       expect(() => GenerateReadmeTemplateSchema.parse({})).toThrow();
 32 |       expect(() =>
 33 |         GenerateReadmeTemplateSchema.parse({
 34 |           projectName: "",
 35 |           description: "test",
 36 |         }),
 37 |       ).toThrow();
 38 |       expect(() =>
 39 |         GenerateReadmeTemplateSchema.parse({
 40 |           projectName: "test",
 41 |           description: "",
 42 |         }),
 43 |       ).toThrow();
 44 |     });
 45 | 
 46 |     it("should accept valid input with defaults", () => {
 47 |       const input = GenerateReadmeTemplateSchema.parse({
 48 |         projectName: "test-project",
 49 |         description: "A test project",
 50 |         templateType: "library",
 51 |       });
 52 | 
 53 |       expect(input.license).toBe("MIT");
 54 |       expect(input.includeScreenshots).toBe(false);
 55 |       expect(input.includeBadges).toBe(true);
 56 |       expect(input.includeContributing).toBe(true);
 57 |     });
 58 | 
 59 |     it("should validate template types", () => {
 60 |       expect(() =>
 61 |         GenerateReadmeTemplateSchema.parse({
 62 |           projectName: "test",
 63 |           description: "test",
 64 |           templateType: "invalid-type",
 65 |         }),
 66 |       ).toThrow();
 67 | 
 68 |       const validTypes: TemplateType[] = [
 69 |         "library",
 70 |         "application",
 71 |         "cli-tool",
 72 |         "api",
 73 |         "documentation",
 74 |       ];
 75 |       for (const type of validTypes) {
 76 |         expect(() =>
 77 |           GenerateReadmeTemplateSchema.parse({
 78 |             projectName: "test",
 79 |             description: "test",
 80 |             templateType: type,
 81 |           }),
 82 |         ).not.toThrow();
 83 |       }
 84 |     });
 85 |   });
 86 | 
 87 |   describe("Template Generation", () => {
 88 |     it("should generate library template correctly", async () => {
 89 |       const input = GenerateReadmeTemplateSchema.parse({
 90 |         projectName: "awesome-lib",
 91 |         description: "An awesome JavaScript library",
 92 |         templateType: "library",
 93 |         author: "john-doe",
 94 |       });
 95 |       const result = await generateReadmeTemplate(input);
 96 | 
 97 |       expect(result.content).toContain("# awesome-lib");
 98 |       expect(result.content).toContain("> An awesome JavaScript library");
 99 |       expect(result.content).toContain("npm install awesome-lib");
100 |       expect(result.content).toContain(
101 |         "const awesomeLib = require('awesome-lib')",
102 |       );
103 |       expect(result.content).toContain("## TL;DR");
104 |       expect(result.content).toContain("## Quick Start");
105 |       expect(result.content).toContain("## API Documentation");
106 |       expect(result.content).toContain("MIT © john-doe");
107 |       expect(result.metadata.templateType).toBe("library");
108 |       expect(result.metadata.estimatedLength).toBe(150);
109 |     });
110 | 
111 |     it("should generate application template correctly", async () => {
112 |       const input = GenerateReadmeTemplateSchema.parse({
113 |         projectName: "my-app",
114 |         description: "A web application",
115 |         templateType: "application",
116 |         author: "jane-doe",
117 |         includeScreenshots: true,
118 |       });
119 |       const result = await generateReadmeTemplate(input);
120 | 
121 |       expect(result.content).toContain("# my-app");
122 |       expect(result.content).toContain("> A web application");
123 |       expect(result.content).toContain("## What This Does");
124 |       expect(result.content).toContain(
125 |         "git clone https://github.com/jane-doe/my-app.git",
126 |       );
127 |       expect(result.content).toContain("npm start");
128 |       expect(result.content).toContain("## Configuration");
129 |       expect(result.content).toContain("![my-app Screenshot]");
130 |       expect(result.metadata.templateType).toBe("application");
131 |     });
132 | 
133 |     it("should generate CLI tool template correctly", async () => {
134 |       const input = GenerateReadmeTemplateSchema.parse({
135 |         projectName: "my-cli",
136 |         description: "A command line tool",
137 |         templateType: "cli-tool",
138 |         author: "dev-user",
139 |       });
140 |       const result = await generateReadmeTemplate(input);
141 | 
142 |       expect(result.content).toContain("# my-cli");
143 |       expect(result.content).toContain("npm install -g my-cli");
144 |       expect(result.content).toContain("npx my-cli --help");
145 |       expect(result.content).toContain("## Usage");
146 |       expect(result.content).toContain("## Options");
147 |       expect(result.content).toContain("| Option | Description | Default |");
148 |       expect(result.metadata.templateType).toBe("cli-tool");
149 |     });
150 | 
151 |     it("should handle camelCase conversion correctly", () => {
152 |       const testCases = [
153 |         { input: "my-awesome-lib", expected: "myAwesomeLib" },
154 |         { input: "simple_package", expected: "simplePackage" },
155 |         { input: "Mixed-Case_Name", expected: "mixedCaseName" },
156 |         { input: "single", expected: "single" },
157 |       ];
158 | 
159 |       for (const testCase of testCases) {
160 |         const generator = new ReadmeTemplateGenerator();
161 |         const input = GenerateReadmeTemplateSchema.parse({
162 |           projectName: testCase.input,
163 |           description: "test",
164 |           templateType: "library",
165 |         });
166 |         const result = generator.generateTemplate(input);
167 | 
168 |         expect(result).toContain(
169 |           `const ${testCase.expected} = require('${testCase.input}')`,
170 |         );
171 |       }
172 |     });
173 |   });
174 | 
175 |   describe("Badge Generation", () => {
176 |     it("should include badges when enabled", async () => {
177 |       const input = GenerateReadmeTemplateSchema.parse({
178 |         projectName: "badge-lib",
179 |         description: "Library with badges",
180 |         templateType: "library",
181 |         author: "dev",
182 |         includeBadges: true,
183 |       });
184 |       const result = await generateReadmeTemplate(input);
185 | 
186 |       expect(result.content).toContain("[![npm version]");
187 |       expect(result.content).toContain("[![Build Status]");
188 |       expect(result.content).toContain("[![License: MIT]");
189 |       expect(result.content).toContain("dev/badge-lib");
190 |     });
191 | 
192 |     it("should exclude badges when disabled", async () => {
193 |       const input = GenerateReadmeTemplateSchema.parse({
194 |         projectName: "no-badge-lib",
195 |         description: "Library without badges",
196 |         templateType: "library",
197 |         includeBadges: false,
198 |       });
199 |       const result = await generateReadmeTemplate(input);
200 | 
201 |       expect(result.content).not.toContain("[![");
202 |     });
203 | 
204 |     it("should customize badge URLs with author", async () => {
205 |       const input = GenerateReadmeTemplateSchema.parse({
206 |         projectName: "app-with-badges",
207 |         description: "App with custom badges",
208 |         templateType: "application",
209 |         author: "dev",
210 |         includeBadges: true,
211 |       });
212 |       const result = await generateReadmeTemplate(input);
213 | 
214 |       expect(result.content).toContain("dev/app-with-badges");
215 |     });
216 |   });
217 | 
218 |   describe("Screenshot Handling", () => {
219 |     it("should include screenshot placeholder when enabled", async () => {
220 |       const input = GenerateReadmeTemplateSchema.parse({
221 |         projectName: "screenshot-app",
222 |         description: "App with screenshots",
223 |         templateType: "application",
224 |         includeScreenshots: true,
225 |       });
226 |       const result = await generateReadmeTemplate(input);
227 | 
228 |       expect(result.content).toContain(
229 |         "![screenshot-app Screenshot](docs/screenshot.png)",
230 |       );
231 |       expect(result.content).toContain("*Add a screenshot or demo GIF here*");
232 |     });
233 | 
234 |     it("should exclude screenshots when disabled", async () => {
235 |       const input = GenerateReadmeTemplateSchema.parse({
236 |         projectName: "no-screenshot-app",
237 |         description: "App without screenshots",
238 |         templateType: "application",
239 |         includeScreenshots: false,
240 |       });
241 |       const result = await generateReadmeTemplate(input);
242 | 
243 |       expect(result.content).not.toContain("![visual-app Screenshot]");
244 |     });
245 |   });
246 | 
247 |   describe("Contributing Section", () => {
248 |     it("should include contributing section when enabled", async () => {
249 |       const input = GenerateReadmeTemplateSchema.parse({
250 |         projectName: "contrib-lib",
251 |         description: "Library with contributing section",
252 |         templateType: "library",
253 |         includeContributing: true,
254 |       });
255 |       const result = await generateReadmeTemplate(input);
256 | 
257 |       expect(result.content).toContain("## Contributing");
258 |       expect(result.content).toContain("CONTRIBUTING.md");
259 |     });
260 | 
261 |     it("should exclude contributing section when disabled", async () => {
262 |       const input = GenerateReadmeTemplateSchema.parse({
263 |         projectName: "no-contrib-lib",
264 |         description: "Library without contributing section",
265 |         templateType: "library",
266 |         includeContributing: false,
267 |       });
268 |       const result = await generateReadmeTemplate(input);
269 | 
270 |       expect(result.content).not.toContain("## Contributing");
271 |     });
272 |   });
273 | 
274 |   describe("File Output", () => {
275 |     it("should write to file when outputPath is specified", async () => {
276 |       const outputPath = path.join(tempDir, "README.md");
277 | 
278 |       const input = GenerateReadmeTemplateSchema.parse({
279 |         projectName: "output-lib",
280 |         description: "Library with file output",
281 |         templateType: "library",
282 |         outputPath: outputPath,
283 |       });
284 |       const result = await generateReadmeTemplate(input);
285 | 
286 |       await expect(fs.access(outputPath)).resolves.toBeUndefined();
287 |       const fileContent = await fs.readFile(outputPath, "utf-8");
288 |       expect(fileContent).toBe(result.content);
289 |       expect(fileContent).toContain("# output-lib");
290 |     });
291 | 
292 |     it("should not write file when outputPath is not specified", async () => {
293 |       const input = GenerateReadmeTemplateSchema.parse({
294 |         projectName: "no-file-test",
295 |         description: "Library without file output",
296 |         templateType: "library",
297 |       });
298 |       await generateReadmeTemplate(input);
299 | 
300 |       const possiblePath = path.join(tempDir, "README.md");
301 |       await expect(fs.access(possiblePath)).rejects.toThrow();
302 |     });
303 |   });
304 | 
305 |   describe("Template Metadata", () => {
306 |     it("should return correct metadata for each template type", () => {
307 |       const templateTypes: TemplateType[] = [
308 |         "library",
309 |         "application",
310 |         "cli-tool",
311 |       ];
312 | 
313 |       for (const type of templateTypes) {
314 |         const info = generator.getTemplateInfo(type);
315 |         expect(info).toBeDefined();
316 |         expect(info!.type).toBe(type);
317 |         expect(info!.estimatedLength).toBeGreaterThan(0);
318 |       }
319 |     });
320 | 
321 |     it("should return null for invalid template type", () => {
322 |       const info = generator.getTemplateInfo("invalid" as TemplateType);
323 |       expect(info).toBeNull();
324 |     });
325 | 
326 |     it("should count sections correctly", async () => {
327 |       const input = GenerateReadmeTemplateSchema.parse({
328 |         projectName: "error-lib",
329 |         description: "Library that causes error",
330 |         templateType: "library",
331 |       });
332 |       const result = await generateReadmeTemplate(input);
333 | 
334 |       const sectionCount = (result.content.match(/^##\s/gm) || []).length;
335 |       expect(result.metadata.sectionsIncluded).toBeGreaterThanOrEqual(
336 |         sectionCount,
337 |       );
338 |       expect(result.metadata.sectionsIncluded).toBeGreaterThan(3);
339 |     });
340 |   });
341 | 
342 |   describe("Available Templates", () => {
343 |     it("should return list of available template types", () => {
344 |       const availableTypes = generator.getAvailableTemplates();
345 |       expect(availableTypes).toContain("library");
346 |       expect(availableTypes).toContain("application");
347 |       expect(availableTypes).toContain("cli-tool");
348 |       expect(availableTypes.length).toBeGreaterThan(0);
349 |     });
350 |   });
351 | 
352 |   describe("Error Handling", () => {
353 |     it("should throw error for unsupported template type", async () => {
354 |       const generator = new ReadmeTemplateGenerator();
355 |       expect(() =>
356 |         generator.generateTemplate({
357 |           projectName: "test",
358 |           description: "test",
359 |           templateType: "unsupported" as TemplateType,
360 |           license: "MIT",
361 |           includeScreenshots: false,
362 |           includeBadges: true,
363 |           includeContributing: true,
364 |         }),
365 |       ).toThrow('Template type "unsupported" not supported');
366 |     });
367 | 
368 |     it("should handle file write errors gracefully", async () => {
369 |       const invalidPath = "/invalid/nonexistent/path/README.md";
370 | 
371 |       const input = GenerateReadmeTemplateSchema.parse({
372 |         projectName: "error-test",
373 |         description: "test error handling",
374 |         templateType: "library",
375 |         outputPath: invalidPath,
376 |       });
377 |       await expect(generateReadmeTemplate(input)).rejects.toThrow();
378 |     });
379 |   });
380 | 
381 |   describe("Variable Replacement", () => {
382 |     it("should replace all template variables correctly", async () => {
383 |       const input = GenerateReadmeTemplateSchema.parse({
384 |         projectName: "license-lib",
385 |         description: "Library with custom license",
386 |         templateType: "library",
387 |         author: "dev",
388 |         license: "Apache-2.0",
389 |       });
390 |       const result = await generateReadmeTemplate(input);
391 | 
392 |       expect(result.content).not.toContain("{{projectName}}");
393 |       expect(result.content).not.toContain("{{description}}");
394 |       expect(result.content).not.toContain("{{author}}");
395 |       expect(result.content).not.toContain("{{license}}");
396 |       expect(result.content).toContain("license-lib");
397 |       expect(result.content).toContain("Library with custom license");
398 |       expect(result.content).toContain("dev");
399 |       expect(result.content).toContain("Apache-2.0");
400 |     });
401 | 
402 |     it("should use default values for missing optional fields", async () => {
403 |       const input = GenerateReadmeTemplateSchema.parse({
404 |         projectName: "time-lib",
405 |         description: "Library with timing",
406 |         templateType: "library",
407 |       });
408 |       const result = await generateReadmeTemplate(input);
409 | 
410 |       expect(result.content).toContain("your-username");
411 |       expect(result.content).toContain("MIT");
412 |     });
413 |   });
414 | 
415 |   describe("Template Structure Validation", () => {
416 |     it("should generate valid markdown structure", async () => {
417 |       const input = GenerateReadmeTemplateSchema.parse({
418 |         projectName: "structure-test",
419 |         description: "test structure",
420 |         templateType: "library",
421 |       });
422 |       const result = await generateReadmeTemplate(input);
423 | 
424 |       // Check for proper heading hierarchy
425 |       const lines = result.content.split("\n");
426 |       const headings = lines.filter((line) => line.startsWith("#"));
427 | 
428 |       expect(headings.length).toBeGreaterThan(0);
429 |       expect(headings[0]).toMatch(/^#\s+/); // Main title
430 | 
431 |       // Check for code blocks
432 |       expect(result.content).toMatch(/```[\s\S]*?```/);
433 | 
434 |       // Check for proper spacing
435 |       expect(result.content).not.toMatch(/#{1,6}\s*\n\s*#{1,6}/);
436 |     });
437 | 
438 |     it("should maintain consistent formatting across templates", async () => {
439 |       const templateTypes: TemplateType[] = [
440 |         "library",
441 |         "application",
442 |         "cli-tool",
443 |       ];
444 | 
445 |       for (const type of templateTypes) {
446 |         const input = GenerateReadmeTemplateSchema.parse({
447 |           projectName: "format-test",
448 |           description: "test format",
449 |           templateType: type,
450 |         });
451 |         const result = await generateReadmeTemplate(input);
452 | 
453 |         // All templates should have main title
454 |         expect(result.content).toMatch(/^#\s+format-test/m);
455 | 
456 |         // All templates should have license section
457 |         expect(result.content).toContain("## License");
458 | 
459 |         // All templates should end with license info
460 |         expect(result.content.trim()).toMatch(/MIT © your-username$/);
461 |       }
462 |     });
463 |   });
464 | });
465 | 
```

--------------------------------------------------------------------------------
/tests/memory/user-preferences.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for User Preference Management
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
  6 | import { promises as fs } from "fs";
  7 | import { join } from "path";
  8 | import { tmpdir } from "os";
  9 | import {
 10 |   UserPreferenceManager,
 11 |   getUserPreferenceManager,
 12 |   clearPreferenceManagerCache,
 13 | } from "../../src/memory/user-preferences.js";
 14 | import {
 15 |   getKnowledgeGraph,
 16 |   initializeKnowledgeGraph,
 17 | } from "../../src/memory/kg-integration.js";
 18 | 
 19 | describe("UserPreferenceManager", () => {
 20 |   let testDir: string;
 21 | 
 22 |   beforeEach(async () => {
 23 |     // Create temporary test directory
 24 |     testDir = join(tmpdir(), `user-prefs-test-${Date.now()}`);
 25 |     await fs.mkdir(testDir, { recursive: true });
 26 | 
 27 |     // Initialize KG with test directory
 28 |     await initializeKnowledgeGraph(testDir);
 29 |     clearPreferenceManagerCache();
 30 |   });
 31 | 
 32 |   afterEach(async () => {
 33 |     clearPreferenceManagerCache();
 34 |     // Clean up test directory
 35 |     try {
 36 |       await fs.rm(testDir, { recursive: true, force: true });
 37 |     } catch (error) {
 38 |       // Ignore cleanup errors
 39 |     }
 40 |   });
 41 | 
 42 |   describe("Initialization", () => {
 43 |     it("should create default preferences for new user", async () => {
 44 |       const manager = new UserPreferenceManager("test-user");
 45 |       await manager.initialize();
 46 | 
 47 |       const prefs = await manager.getPreferences();
 48 |       expect(prefs.userId).toBe("test-user");
 49 |       expect(prefs.preferredSSGs).toEqual([]);
 50 |       expect(prefs.documentationStyle).toBe("comprehensive");
 51 |       expect(prefs.expertiseLevel).toBe("intermediate");
 52 |       expect(prefs.autoApplyPreferences).toBe(true);
 53 |     });
 54 | 
 55 |     it("should load existing preferences from knowledge graph", async () => {
 56 |       // Create a user with preferences
 57 |       const kg = await getKnowledgeGraph();
 58 |       kg.addNode({
 59 |         id: "user:existing-user",
 60 |         type: "user",
 61 |         label: "existing-user",
 62 |         properties: {
 63 |           userId: "existing-user",
 64 |           preferredSSGs: ["jekyll", "hugo"],
 65 |           documentationStyle: "minimal",
 66 |           expertiseLevel: "advanced",
 67 |           preferredTechnologies: ["typescript"],
 68 |           preferredDiataxisCategories: ["tutorials"],
 69 |           autoApplyPreferences: false,
 70 |           lastActive: "2025-01-01T00:00:00.000Z",
 71 |         },
 72 |         weight: 1.0,
 73 |       });
 74 | 
 75 |       const manager = new UserPreferenceManager("existing-user");
 76 |       await manager.initialize();
 77 | 
 78 |       const prefs = await manager.getPreferences();
 79 |       expect(prefs.userId).toBe("existing-user");
 80 |       expect(prefs.preferredSSGs).toEqual(["jekyll", "hugo"]);
 81 |       expect(prefs.documentationStyle).toBe("minimal");
 82 |       expect(prefs.expertiseLevel).toBe("advanced");
 83 |       expect(prefs.autoApplyPreferences).toBe(false);
 84 |     });
 85 | 
 86 |     it("should handle getPreferences before initialization", async () => {
 87 |       const manager = new UserPreferenceManager("auto-init");
 88 |       const prefs = await manager.getPreferences();
 89 | 
 90 |       expect(prefs.userId).toBe("auto-init");
 91 |       expect(prefs.preferredSSGs).toEqual([]);
 92 |     });
 93 |   });
 94 | 
 95 |   describe("Update Preferences", () => {
 96 |     it("should update preferences and save to knowledge graph", async () => {
 97 |       const manager = new UserPreferenceManager("update-test");
 98 |       await manager.initialize();
 99 | 
100 |       await manager.updatePreferences({
101 |         documentationStyle: "tutorial-heavy",
102 |         expertiseLevel: "beginner",
103 |         preferredTechnologies: ["python", "go"],
104 |       });
105 | 
106 |       const prefs = await manager.getPreferences();
107 |       expect(prefs.documentationStyle).toBe("tutorial-heavy");
108 |       expect(prefs.expertiseLevel).toBe("beginner");
109 |       expect(prefs.preferredTechnologies).toEqual(["python", "go"]);
110 |     });
111 | 
112 |     it("should initialize before update if not already initialized", async () => {
113 |       const manager = new UserPreferenceManager("lazy-init");
114 | 
115 |       await manager.updatePreferences({
116 |         expertiseLevel: "advanced",
117 |       });
118 | 
119 |       const prefs = await manager.getPreferences();
120 |       expect(prefs.expertiseLevel).toBe("advanced");
121 |     });
122 |   });
123 | 
124 |   describe("Track SSG Usage", () => {
125 |     it("should track successful SSG usage and create preference", async () => {
126 |       const manager = new UserPreferenceManager("ssg-tracker");
127 |       await manager.initialize();
128 | 
129 |       await manager.trackSSGUsage({
130 |         ssg: "jekyll",
131 |         success: true,
132 |         timestamp: "2025-01-01T00:00:00.000Z",
133 |       });
134 | 
135 |       const prefs = await manager.getPreferences();
136 |       expect(prefs.preferredSSGs).toContain("jekyll");
137 |     });
138 | 
139 |     it("should track failed SSG usage", async () => {
140 |       const manager = new UserPreferenceManager("fail-tracker");
141 |       await manager.initialize();
142 | 
143 |       await manager.trackSSGUsage({
144 |         ssg: "hugo",
145 |         success: false,
146 |         timestamp: "2025-01-01T00:00:00.000Z",
147 |       });
148 | 
149 |       const kg = await getKnowledgeGraph();
150 |       const edges = await kg.findEdges({
151 |         type: "user_prefers_ssg",
152 |       });
153 | 
154 |       expect(edges.length).toBeGreaterThan(0);
155 |       const edge = edges.find((e) => e.target.includes("hugo"));
156 |       expect(edge).toBeDefined();
157 |       expect(edge!.weight).toBe(0.5); // Failed usage has lower weight
158 |     });
159 | 
160 |     it("should update existing SSG preference", async () => {
161 |       const manager = new UserPreferenceManager("update-tracker");
162 |       await manager.initialize();
163 | 
164 |       // First usage - success
165 |       await manager.trackSSGUsage({
166 |         ssg: "docusaurus",
167 |         success: true,
168 |         timestamp: "2025-01-01T00:00:00.000Z",
169 |       });
170 | 
171 |       // Second usage - success
172 |       await manager.trackSSGUsage({
173 |         ssg: "docusaurus",
174 |         success: true,
175 |         timestamp: "2025-01-02T00:00:00.000Z",
176 |       });
177 | 
178 |       const kg = await getKnowledgeGraph();
179 |       const edges = await kg.findEdges({
180 |         type: "user_prefers_ssg",
181 |       });
182 | 
183 |       const docEdge = edges.find((e) => e.target.includes("docusaurus"));
184 |       expect(docEdge!.properties.usageCount).toBe(2);
185 |       expect(docEdge!.properties.successRate).toBe(1.0);
186 |     });
187 | 
188 |     it("should calculate average success rate correctly", async () => {
189 |       const manager = new UserPreferenceManager("avg-tracker");
190 |       await manager.initialize();
191 | 
192 |       // Success
193 |       await manager.trackSSGUsage({
194 |         ssg: "mkdocs",
195 |         success: true,
196 |         timestamp: "2025-01-01T00:00:00.000Z",
197 |       });
198 | 
199 |       // Failure
200 |       await manager.trackSSGUsage({
201 |         ssg: "mkdocs",
202 |         success: false,
203 |         timestamp: "2025-01-02T00:00:00.000Z",
204 |       });
205 | 
206 |       const kg = await getKnowledgeGraph();
207 |       const edges = await kg.findEdges({
208 |         type: "user_prefers_ssg",
209 |       });
210 | 
211 |       const mkdocsEdge = edges.find((e) => e.target.includes("mkdocs"));
212 |       expect(mkdocsEdge!.properties.successRate).toBe(0.5);
213 |     });
214 | 
215 |     it("should create user node if it doesn't exist during tracking", async () => {
216 |       const manager = new UserPreferenceManager("new-tracker");
217 |       // Don't initialize - let trackSSGUsage create it
218 | 
219 |       await manager.trackSSGUsage({
220 |         ssg: "eleventy",
221 |         success: true,
222 |         timestamp: "2025-01-01T00:00:00.000Z",
223 |       });
224 | 
225 |       const kg = await getKnowledgeGraph();
226 |       const userNode = await kg.findNode({
227 |         type: "user",
228 |         properties: { userId: "new-tracker" },
229 |       });
230 | 
231 |       expect(userNode).toBeDefined();
232 |     });
233 |   });
234 | 
235 |   describe("SSG Recommendations", () => {
236 |     it("should return recommendations sorted by score", async () => {
237 |       const manager = new UserPreferenceManager("rec-test");
238 |       await manager.initialize();
239 | 
240 |       // Track multiple SSGs with different success rates
241 |       await manager.trackSSGUsage({
242 |         ssg: "jekyll",
243 |         success: true,
244 |         timestamp: "2025-01-01T00:00:00.000Z",
245 |       });
246 |       await manager.trackSSGUsage({
247 |         ssg: "jekyll",
248 |         success: true,
249 |         timestamp: "2025-01-02T00:00:00.000Z",
250 |       });
251 |       await manager.trackSSGUsage({
252 |         ssg: "hugo",
253 |         success: true,
254 |         timestamp: "2025-01-03T00:00:00.000Z",
255 |       });
256 | 
257 |       const recommendations = await manager.getSSGRecommendations();
258 | 
259 |       expect(recommendations.length).toBeGreaterThan(0);
260 |       expect(recommendations[0].ssg).toBe("jekyll"); // Higher usage count
261 |       expect(recommendations[0].score).toBeGreaterThan(
262 |         recommendations[1].score,
263 |       );
264 |     });
265 | 
266 |     it("should include reason with high success rate", async () => {
267 |       const manager = new UserPreferenceManager("reason-test");
268 |       await manager.initialize();
269 | 
270 |       await manager.trackSSGUsage({
271 |         ssg: "docusaurus",
272 |         success: true,
273 |         timestamp: "2025-01-01T00:00:00.000Z",
274 |       });
275 | 
276 |       const recommendations = await manager.getSSGRecommendations();
277 |       const docRec = recommendations.find((r) => r.ssg === "docusaurus");
278 | 
279 |       expect(docRec!.reason).toContain("100% success rate");
280 |     });
281 | 
282 |     it("should include reason with low success rate", async () => {
283 |       const manager = new UserPreferenceManager("low-success-test");
284 |       await manager.initialize();
285 | 
286 |       // Track both success and failure to get a low rate (not exactly 0)
287 |       await manager.trackSSGUsage({
288 |         ssg: "eleventy",
289 |         success: true,
290 |         timestamp: "2025-01-01T00:00:00.000Z",
291 |       });
292 |       await manager.trackSSGUsage({
293 |         ssg: "eleventy",
294 |         success: false,
295 |         timestamp: "2025-01-02T00:00:00.000Z",
296 |       });
297 |       await manager.trackSSGUsage({
298 |         ssg: "eleventy",
299 |         success: false,
300 |         timestamp: "2025-01-03T00:00:00.000Z",
301 |       });
302 | 
303 |       const recommendations = await manager.getSSGRecommendations();
304 |       const eleventyRec = recommendations.find((r) => r.ssg === "eleventy");
305 | 
306 |       expect(eleventyRec!.reason).toContain("only");
307 |       expect(eleventyRec!.reason).toContain("success rate");
308 |     });
309 | 
310 |     it("should return empty array if no user node exists", async () => {
311 |       const manager = new UserPreferenceManager("no-user");
312 |       // Don't initialize or create user node
313 | 
314 |       const recommendations = await manager.getSSGRecommendations();
315 | 
316 |       expect(recommendations).toEqual([]);
317 |     });
318 |   });
319 | 
320 |   describe("Apply Preferences to Recommendation", () => {
321 |     it("should return original recommendation if autoApply is false", async () => {
322 |       const manager = new UserPreferenceManager("no-auto");
323 |       await manager.updatePreferences({
324 |         autoApplyPreferences: false,
325 |         preferredSSGs: ["jekyll"],
326 |       });
327 | 
328 |       const result = manager.applyPreferencesToRecommendation("hugo", [
329 |         "jekyll",
330 |         "hugo",
331 |       ]);
332 | 
333 |       expect(result.recommended).toBe("hugo");
334 |       expect(result.adjustmentReason).toBeUndefined();
335 |     });
336 | 
337 |     it("should keep recommendation if it matches preferred SSG", async () => {
338 |       const manager = new UserPreferenceManager("match-pref");
339 |       await manager.updatePreferences({
340 |         preferredSSGs: ["jekyll", "hugo"],
341 |       });
342 | 
343 |       const result = manager.applyPreferencesToRecommendation("jekyll", [
344 |         "jekyll",
345 |         "hugo",
346 |         "mkdocs",
347 |       ]);
348 | 
349 |       expect(result.recommended).toBe("jekyll");
350 |       expect(result.adjustmentReason).toContain("Matches your preferred SSG");
351 |     });
352 | 
353 |     it("should switch to preferred SSG if in alternatives", async () => {
354 |       const manager = new UserPreferenceManager("switch-pref");
355 |       await manager.updatePreferences({
356 |         preferredSSGs: ["docusaurus"],
357 |       });
358 | 
359 |       const result = manager.applyPreferencesToRecommendation("jekyll", [
360 |         "jekyll",
361 |         "docusaurus",
362 |         "hugo",
363 |       ]);
364 | 
365 |       expect(result.recommended).toBe("docusaurus");
366 |       expect(result.adjustmentReason).toContain(
367 |         "Switched to docusaurus based on your usage history",
368 |       );
369 |     });
370 | 
371 |     it("should return original if no preferred SSGs match", async () => {
372 |       const manager = new UserPreferenceManager("no-match");
373 |       await manager.updatePreferences({
374 |         preferredSSGs: ["eleventy"],
375 |       });
376 | 
377 |       const result = manager.applyPreferencesToRecommendation("jekyll", [
378 |         "jekyll",
379 |         "hugo",
380 |       ]);
381 | 
382 |       expect(result.recommended).toBe("jekyll");
383 |       expect(result.adjustmentReason).toBeUndefined();
384 |     });
385 | 
386 |     it("should return original if no preferences set", async () => {
387 |       const manager = new UserPreferenceManager("empty-pref");
388 |       await manager.initialize();
389 | 
390 |       const result = manager.applyPreferencesToRecommendation("jekyll", [
391 |         "jekyll",
392 |         "hugo",
393 |       ]);
394 | 
395 |       expect(result.recommended).toBe("jekyll");
396 |       expect(result.adjustmentReason).toBeUndefined();
397 |     });
398 |   });
399 | 
400 |   describe("Reset Preferences", () => {
401 |     it("should reset preferences to defaults", async () => {
402 |       const manager = new UserPreferenceManager("reset-test");
403 |       await manager.updatePreferences({
404 |         documentationStyle: "minimal",
405 |         expertiseLevel: "advanced",
406 |         preferredSSGs: ["jekyll", "hugo"],
407 |       });
408 | 
409 |       await manager.resetPreferences();
410 | 
411 |       const prefs = await manager.getPreferences();
412 |       expect(prefs.documentationStyle).toBe("comprehensive");
413 |       expect(prefs.expertiseLevel).toBe("intermediate");
414 |       expect(prefs.preferredSSGs).toEqual([]);
415 |     });
416 |   });
417 | 
418 |   describe("Export/Import Preferences", () => {
419 |     it("should export preferences as JSON", async () => {
420 |       const manager = new UserPreferenceManager("export-test");
421 |       await manager.updatePreferences({
422 |         expertiseLevel: "advanced",
423 |         preferredSSGs: ["jekyll"],
424 |       });
425 | 
426 |       const exported = await manager.exportPreferences();
427 |       const parsed = JSON.parse(exported);
428 | 
429 |       expect(parsed.userId).toBe("export-test");
430 |       expect(parsed.expertiseLevel).toBe("advanced");
431 |       expect(parsed.preferredSSGs).toEqual(["jekyll"]);
432 |     });
433 | 
434 |     it("should import preferences from JSON", async () => {
435 |       const manager = new UserPreferenceManager("import-test");
436 |       await manager.initialize();
437 | 
438 |       const importData = {
439 |         userId: "import-test",
440 |         preferredSSGs: ["hugo", "docusaurus"],
441 |         documentationStyle: "tutorial-heavy" as const,
442 |         expertiseLevel: "beginner" as const,
443 |         preferredTechnologies: ["python"],
444 |         preferredDiataxisCategories: ["tutorials" as const],
445 |         autoApplyPreferences: false,
446 |         lastUpdated: "2025-01-01T00:00:00.000Z",
447 |       };
448 | 
449 |       await manager.importPreferences(JSON.stringify(importData));
450 | 
451 |       const prefs = await manager.getPreferences();
452 |       expect(prefs.expertiseLevel).toBe("beginner");
453 |       expect(prefs.preferredSSGs).toEqual(["hugo", "docusaurus"]);
454 |       expect(prefs.autoApplyPreferences).toBe(false);
455 |     });
456 | 
457 |     it("should throw error on userId mismatch during import", async () => {
458 |       const manager = new UserPreferenceManager("user1");
459 |       await manager.initialize();
460 | 
461 |       const importData = {
462 |         userId: "user2", // Different user ID
463 |         preferredSSGs: [],
464 |         documentationStyle: "comprehensive" as const,
465 |         expertiseLevel: "intermediate" as const,
466 |         preferredTechnologies: [],
467 |         preferredDiataxisCategories: [],
468 |         autoApplyPreferences: true,
469 |         lastUpdated: "2025-01-01T00:00:00.000Z",
470 |       };
471 | 
472 |       await expect(
473 |         manager.importPreferences(JSON.stringify(importData)),
474 |       ).rejects.toThrow("User ID mismatch");
475 |     });
476 |   });
477 | 
478 |   describe("Manager Cache", () => {
479 |     it("should cache preference managers", async () => {
480 |       const manager1 = await getUserPreferenceManager("cached-user");
481 |       const manager2 = await getUserPreferenceManager("cached-user");
482 | 
483 |       expect(manager1).toBe(manager2); // Same instance
484 |     });
485 | 
486 |     it("should create different managers for different users", async () => {
487 |       const manager1 = await getUserPreferenceManager("user1");
488 |       const manager2 = await getUserPreferenceManager("user2");
489 | 
490 |       expect(manager1).not.toBe(manager2);
491 |     });
492 | 
493 |     it("should clear cache", async () => {
494 |       const manager1 = await getUserPreferenceManager("clear-test");
495 |       clearPreferenceManagerCache();
496 |       const manager2 = await getUserPreferenceManager("clear-test");
497 | 
498 |       expect(manager1).not.toBe(manager2); // Different instances after clear
499 |     });
500 |   });
501 | });
502 | 
```
Page 9/29FirstPrevNextLast