This is page 8 of 23. Use http://codebase.md/tosin2013/documcp?page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
│ ├── agents
│ │ ├── documcp-ast.md
│ │ ├── documcp-deploy.md
│ │ ├── documcp-memory.md
│ │ ├── documcp-test.md
│ │ └── documcp-tool.md
│ ├── copilot-instructions.md
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── automated-changelog.md
│ │ ├── bug_report.md
│ │ ├── bug_report.yml
│ │ ├── documentation_issue.md
│ │ ├── feature_request.md
│ │ ├── feature_request.yml
│ │ ├── npm-publishing-fix.md
│ │ └── release_improvements.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── release-drafter.yml
│ └── workflows
│ ├── auto-merge.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── deploy-docs.yml
│ ├── README.md
│ ├── release-drafter.yml
│ └── release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .linkcheck.config.json
├── .markdown-link-check.json
├── .nvmrc
├── .pre-commit-config.yaml
├── .versionrc.json
├── ARCHITECTURAL_CHANGES_SUMMARY.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│ ├── .docusaurus
│ │ ├── docusaurus-plugin-content-docs
│ │ │ └── default
│ │ │ └── __mdx-loader-dependency.json
│ │ └── docusaurus-plugin-content-pages
│ │ └── default
│ │ └── __plugin.json
│ ├── adrs
│ │ ├── adr-0001-mcp-server-architecture.md
│ │ ├── adr-0002-repository-analysis-engine.md
│ │ ├── adr-0003-static-site-generator-recommendation-engine.md
│ │ ├── adr-0004-diataxis-framework-integration.md
│ │ ├── adr-0005-github-pages-deployment-automation.md
│ │ ├── adr-0006-mcp-tools-api-design.md
│ │ ├── adr-0007-mcp-prompts-and-resources-integration.md
│ │ ├── adr-0008-intelligent-content-population-engine.md
│ │ ├── adr-0009-content-accuracy-validation-framework.md
│ │ ├── adr-0010-mcp-resource-pattern-redesign.md
│ │ ├── adr-0011-ce-mcp-compatibility.md
│ │ ├── adr-0012-priority-scoring-system-for-documentation-drift.md
│ │ ├── adr-0013-release-pipeline-and-package-distribution.md
│ │ └── README.md
│ ├── api
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── hierarchy.js
│ │ │ ├── highlight.css
│ │ │ ├── icons.js
│ │ │ ├── icons.svg
│ │ │ ├── main.js
│ │ │ ├── navigation.js
│ │ │ ├── search.js
│ │ │ └── style.css
│ │ ├── hierarchy.html
│ │ ├── index.html
│ │ ├── modules.html
│ │ └── variables
│ │ └── TOOLS.html
│ ├── assets
│ │ └── logo.svg
│ ├── CE-MCP-FINDINGS.md
│ ├── development
│ │ └── MCP_INSPECTOR_TESTING.md
│ ├── docusaurus.config.js
│ ├── explanation
│ │ ├── architecture.md
│ │ └── index.md
│ ├── guides
│ │ ├── link-validation.md
│ │ ├── playwright-integration.md
│ │ └── playwright-testing-workflow.md
│ ├── how-to
│ │ ├── analytics-setup.md
│ │ ├── change-watcher.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── drift-priority-scoring.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── llm-integration.md
│ │ ├── local-testing.md
│ │ ├── performance-optimization.md
│ │ ├── prompting-guide.md
│ │ ├── repository-analysis.md
│ │ ├── seo-optimization.md
│ │ ├── site-monitoring.md
│ │ ├── troubleshooting.md
│ │ └── usage-examples.md
│ ├── index.md
│ ├── knowledge-graph.md
│ ├── package-lock.json
│ ├── package.json
│ ├── phase-2-intelligence.md
│ ├── reference
│ │ ├── api-overview.md
│ │ ├── cli.md
│ │ ├── configuration.md
│ │ ├── deploy-pages.md
│ │ ├── index.md
│ │ ├── mcp-tools.md
│ │ └── prompt-templates.md
│ ├── research
│ │ ├── cross-domain-integration
│ │ │ └── README.md
│ │ ├── domain-1-mcp-architecture
│ │ │ ├── index.md
│ │ │ └── mcp-performance-research.md
│ │ ├── domain-2-repository-analysis
│ │ │ └── README.md
│ │ ├── domain-3-ssg-recommendation
│ │ │ ├── index.md
│ │ │ └── ssg-performance-analysis.md
│ │ ├── domain-4-diataxis-integration
│ │ │ └── README.md
│ │ ├── domain-5-github-deployment
│ │ │ ├── github-pages-security-analysis.md
│ │ │ └── index.md
│ │ ├── domain-6-api-design
│ │ │ └── README.md
│ │ ├── README.md
│ │ ├── research-integration-summary-2025-01-14.md
│ │ ├── research-progress-template.md
│ │ └── research-questions-2025-01-14.md
│ ├── robots.txt
│ ├── sidebars.js
│ ├── sitemap.xml
│ ├── src
│ │ └── css
│ │ └── custom.css
│ └── tutorials
│ ├── development-setup.md
│ ├── environment-setup.md
│ ├── first-deployment.md
│ ├── getting-started.md
│ ├── index.md
│ ├── memory-workflows.md
│ └── user-onboarding.md
├── ISSUE_IMPLEMENTATION_SUMMARY.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│ └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│ ├── benchmarks
│ │ └── performance.ts
│ ├── index.ts
│ ├── memory
│ │ ├── contextual-retrieval.ts
│ │ ├── deployment-analytics.ts
│ │ ├── enhanced-manager.ts
│ │ ├── export-import.ts
│ │ ├── freshness-kg-integration.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kg-code-integration.ts
│ │ ├── kg-health.ts
│ │ ├── kg-integration.ts
│ │ ├── kg-link-validator.ts
│ │ ├── kg-storage.ts
│ │ ├── knowledge-graph.ts
│ │ ├── learning.ts
│ │ ├── manager.ts
│ │ ├── multi-agent-sharing.ts
│ │ ├── pruning.ts
│ │ ├── schemas.ts
│ │ ├── storage.ts
│ │ ├── temporal-analysis.ts
│ │ ├── user-preferences.ts
│ │ └── visualization.ts
│ ├── prompts
│ │ └── technical-writer-prompts.ts
│ ├── scripts
│ │ └── benchmark.ts
│ ├── templates
│ │ └── playwright
│ │ ├── accessibility.spec.template.ts
│ │ ├── Dockerfile.template
│ │ ├── docs-e2e.workflow.template.yml
│ │ ├── link-validation.spec.template.ts
│ │ └── playwright.config.template.ts
│ ├── tools
│ │ ├── analyze-deployments.ts
│ │ ├── analyze-readme.ts
│ │ ├── analyze-repository.ts
│ │ ├── change-watcher.ts
│ │ ├── check-documentation-links.ts
│ │ ├── cleanup-agent-artifacts.ts
│ │ ├── deploy-pages.ts
│ │ ├── detect-gaps.ts
│ │ ├── evaluate-readme-health.ts
│ │ ├── generate-config.ts
│ │ ├── generate-contextual-content.ts
│ │ ├── generate-llm-context.ts
│ │ ├── generate-readme-template.ts
│ │ ├── generate-technical-writer-prompts.ts
│ │ ├── kg-health-check.ts
│ │ ├── manage-preferences.ts
│ │ ├── manage-sitemap.ts
│ │ ├── optimize-readme.ts
│ │ ├── populate-content.ts
│ │ ├── readme-best-practices.ts
│ │ ├── recommend-ssg.ts
│ │ ├── setup-playwright-tests.ts
│ │ ├── setup-structure.ts
│ │ ├── simulate-execution.ts
│ │ ├── sync-code-to-docs.ts
│ │ ├── test-local-deployment.ts
│ │ ├── track-documentation-freshness.ts
│ │ ├── update-existing-documentation.ts
│ │ ├── validate-content.ts
│ │ ├── validate-documentation-freshness.ts
│ │ ├── validate-readme-checklist.ts
│ │ └── verify-deployment.ts
│ ├── types
│ │ └── api.ts
│ ├── utils
│ │ ├── artifact-detector.ts
│ │ ├── ast-analyzer.ts
│ │ ├── change-watcher.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── execution-simulator.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── llm-client.ts
│ │ ├── permission-checker.ts
│ │ ├── semantic-analyzer.ts
│ │ ├── sitemap-generator.ts
│ │ ├── usage-metadata.ts
│ │ └── user-feedback-integration.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── call-graph-builder.test.ts
│ ├── change-watcher-priority.integration.test.ts
│ ├── change-watcher.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── execution-simulator.test.ts
│ ├── functional
│ │ └── tools.test.ts
│ ├── integration
│ │ ├── kg-documentation-workflow.test.ts
│ │ ├── knowledge-graph-workflow.test.ts
│ │ ├── mcp-readme-tools.test.ts
│ │ ├── memory-mcp-tools.test.ts
│ │ ├── readme-technical-writer.test.ts
│ │ └── workflow.test.ts
│ ├── memory
│ │ ├── contextual-retrieval.test.ts
│ │ ├── enhanced-manager.test.ts
│ │ ├── export-import.test.ts
│ │ ├── freshness-kg-integration.test.ts
│ │ ├── kg-code-integration.test.ts
│ │ ├── kg-health.test.ts
│ │ ├── kg-link-validator.test.ts
│ │ ├── kg-storage-validation.test.ts
│ │ ├── kg-storage.test.ts
│ │ ├── knowledge-graph-documentation-examples.test.ts
│ │ ├── knowledge-graph-enhanced.test.ts
│ │ ├── knowledge-graph.test.ts
│ │ ├── learning.test.ts
│ │ ├── manager-advanced.test.ts
│ │ ├── manager.test.ts
│ │ ├── mcp-resource-integration.test.ts
│ │ ├── mcp-tool-persistence.test.ts
│ │ ├── schemas-documentation-examples.test.ts
│ │ ├── schemas.test.ts
│ │ ├── storage.test.ts
│ │ ├── temporal-analysis.test.ts
│ │ └── user-preferences.test.ts
│ ├── performance
│ │ ├── memory-load-testing.test.ts
│ │ └── memory-stress-testing.test.ts
│ ├── prompts
│ │ ├── guided-workflow-prompts.test.ts
│ │ └── technical-writer-prompts.test.ts
│ ├── server.test.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── all-tools.test.ts
│ │ ├── analyze-coverage.test.ts
│ │ ├── analyze-deployments.test.ts
│ │ ├── analyze-readme.test.ts
│ │ ├── analyze-repository.test.ts
│ │ ├── check-documentation-links.test.ts
│ │ ├── cleanup-agent-artifacts.test.ts
│ │ ├── deploy-pages-kg-retrieval.test.ts
│ │ ├── deploy-pages-tracking.test.ts
│ │ ├── deploy-pages.test.ts
│ │ ├── detect-gaps.test.ts
│ │ ├── evaluate-readme-health.test.ts
│ │ ├── generate-contextual-content.test.ts
│ │ ├── generate-llm-context.test.ts
│ │ ├── generate-readme-template.test.ts
│ │ ├── generate-technical-writer-prompts.test.ts
│ │ ├── kg-health-check.test.ts
│ │ ├── manage-sitemap.test.ts
│ │ ├── optimize-readme.test.ts
│ │ ├── readme-best-practices.test.ts
│ │ ├── recommend-ssg-historical.test.ts
│ │ ├── recommend-ssg-preferences.test.ts
│ │ ├── recommend-ssg.test.ts
│ │ ├── simple-coverage.test.ts
│ │ ├── sync-code-to-docs.test.ts
│ │ ├── test-local-deployment.test.ts
│ │ ├── tool-error-handling.test.ts
│ │ ├── track-documentation-freshness.test.ts
│ │ ├── validate-content.test.ts
│ │ ├── validate-documentation-freshness.test.ts
│ │ └── validate-readme-checklist.test.ts
│ ├── types
│ │ └── type-safety.test.ts
│ └── utils
│ ├── artifact-detector.test.ts
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector-diataxis.test.ts
│ ├── drift-detector-priority.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ ├── llm-client.test.ts
│ ├── semantic-analyzer.test.ts
│ ├── sitemap-generator.test.ts
│ ├── usage-metadata.test.ts
│ └── user-feedback-integration.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/tests/memory/kg-link-validator.test.ts:
--------------------------------------------------------------------------------
```typescript
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import {
validateExternalLinks,
validateAndStoreDocumentationLinks,
extractLinksFromContent,
storeLinkValidationInKG,
getLinkValidationHistory,
} from "../../src/memory/kg-link-validator";
import { getKnowledgeGraph } from "../../src/memory/kg-integration";
describe("KG Link Validator", () => {
let tempDir: string;
const originalCwd = process.cwd();
beforeEach(async () => {
tempDir = path.join(os.tmpdir(), `kg-link-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
process.chdir(tempDir);
});
afterEach(async () => {
process.chdir(originalCwd);
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe("validateExternalLinks", () => {
it("should validate valid URLs", async () => {
const urls = ["https://www.google.com", "https://github.com"];
const result = await validateExternalLinks(urls, {
timeout: 5000,
});
expect(result.totalLinks).toBe(2);
expect(result.results).toHaveLength(2);
expect(result.validLinks + result.brokenLinks + result.unknownLinks).toBe(
2,
);
});
it("should detect broken links", async () => {
const urls = ["https://this-domain-definitely-does-not-exist-12345.com"];
const result = await validateExternalLinks(urls, {
timeout: 3000,
});
expect(result.totalLinks).toBe(1);
expect(result.results).toHaveLength(1);
// Should be either broken or unknown
expect(result.brokenLinks + result.unknownLinks).toBeGreaterThan(0);
});
it("should handle empty URL list", async () => {
const result = await validateExternalLinks([]);
expect(result.totalLinks).toBe(0);
expect(result.results).toHaveLength(0);
});
it("should respect timeout option", async () => {
const urls = ["https://www.google.com"];
const startTime = Date.now();
await validateExternalLinks(urls, {
timeout: 1000,
});
const duration = Date.now() - startTime;
// Should complete reasonably quickly
expect(duration).toBeLessThan(10000);
});
it("should use default timeout when not provided", async () => {
const urls = ["https://www.google.com"];
const result = await validateExternalLinks(urls);
expect(result).toBeDefined();
expect(result.totalLinks).toBe(1);
});
it("should handle validation errors gracefully", async () => {
const urls = ["https://httpstat.us/500"]; // Returns 500 error
const result = await validateExternalLinks(urls, {
timeout: 5000,
});
expect(result.totalLinks).toBe(1);
// Should be marked as broken or unknown
expect(result.brokenLinks + result.unknownLinks).toBeGreaterThan(0);
});
it("should count warning links correctly", async () => {
const urls = ["https://httpstat.us/301"]; // Redirect
const result = await validateExternalLinks(urls, {
timeout: 5000,
});
expect(result.totalLinks).toBe(1);
// Should handle redirects as valid (fetch follows redirects)
expect(
result.validLinks +
result.brokenLinks +
result.warningLinks +
result.unknownLinks,
).toBe(1);
});
it("should handle network errors in validation loop", async () => {
const urls = ["https://invalid-url-12345.test", "https://www.google.com"];
const result = await validateExternalLinks(urls, {
timeout: 3000,
});
expect(result.totalLinks).toBe(2);
expect(result.results).toHaveLength(2);
});
it("should include response time in valid results", async () => {
const urls = ["https://www.google.com"];
const result = await validateExternalLinks(urls, {
timeout: 5000,
});
expect(result.results[0].lastChecked).toBeDefined();
if (result.results[0].status === "valid") {
expect(result.results[0].responseTime).toBeDefined();
expect(result.results[0].responseTime).toBeGreaterThan(0);
}
});
it("should include response time in broken results", async () => {
const urls = ["https://httpstat.us/404"];
const result = await validateExternalLinks(urls, {
timeout: 5000,
});
expect(result.results[0].lastChecked).toBeDefined();
if (
result.results[0].status === "broken" &&
result.results[0].statusCode
) {
expect(result.results[0].responseTime).toBeDefined();
}
});
});
describe("extractLinksFromContent", () => {
it("should extract external links", () => {
const content = `
# Test
[Google](https://www.google.com)
[GitHub](https://github.com)
`;
const result = extractLinksFromContent(content);
expect(result.externalLinks.length).toBeGreaterThan(0);
});
it("should extract internal links", () => {
const content = `
# Test
[Page 1](./page1.md)
[Page 2](../page2.md)
`;
const result = extractLinksFromContent(content);
expect(result.internalLinks.length).toBeGreaterThan(0);
});
it("should handle mixed links", () => {
const content = `
# Test
[External](https://example.com)
[Internal](./page.md)
`;
const result = extractLinksFromContent(content);
expect(result.externalLinks.length).toBeGreaterThan(0);
expect(result.internalLinks.length).toBeGreaterThan(0);
});
it("should extract HTTP links", () => {
const content = `[Link](http://example.com)`;
const result = extractLinksFromContent(content);
expect(result.externalLinks).toContain("http://example.com");
});
it("should extract HTML anchor links", () => {
const content = `<a href="https://example.com">Link</a>`;
const result = extractLinksFromContent(content);
expect(result.externalLinks).toContain("https://example.com");
});
it("should extract HTML anchor links with single quotes", () => {
const content = `<a href='https://example.com'>Link</a>`;
const result = extractLinksFromContent(content);
expect(result.externalLinks).toContain("https://example.com");
});
it("should extract internal HTML links", () => {
const content = `<a href="./page.md">Link</a>`;
const result = extractLinksFromContent(content);
expect(result.internalLinks).toContain("./page.md");
});
it("should remove duplicate links", () => {
const content = `
[Link1](https://example.com)
[Link2](https://example.com)
[Link3](./page.md)
[Link4](./page.md)
`;
const result = extractLinksFromContent(content);
expect(result.externalLinks.length).toBe(1);
expect(result.internalLinks.length).toBe(1);
});
it("should handle content with no links", () => {
const content = "# Test\nNo links here";
const result = extractLinksFromContent(content);
expect(result.externalLinks).toEqual([]);
expect(result.internalLinks).toEqual([]);
});
});
describe("validateAndStoreDocumentationLinks", () => {
it("should validate and store documentation links", async () => {
const content =
"# Test\n[Link](./other.md)\n[External](https://example.com)";
const result = await validateAndStoreDocumentationLinks(
"test-project",
content,
);
expect(result).toBeDefined();
expect(result.totalLinks).toBeGreaterThan(0);
});
it("should handle documentation without links", async () => {
const content = "# Test\nNo links here";
const result = await validateAndStoreDocumentationLinks(
"test-project",
content,
);
expect(result).toBeDefined();
expect(result.totalLinks).toBe(0);
});
it("should handle content with only internal links", async () => {
const content = "# Test\n[Page](./page.md)";
const result = await validateAndStoreDocumentationLinks(
"test-project",
content,
);
expect(result).toBeDefined();
// Only external links are validated
expect(result.totalLinks).toBe(0);
});
});
describe("storeLinkValidationInKG", () => {
it("should store validation results with no broken links", async () => {
const summary = {
totalLinks: 5,
validLinks: 5,
brokenLinks: 0,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-1", summary);
const kg = await getKnowledgeGraph();
const nodes = await kg.getAllNodes();
// Find the specific validation node for this test
const validationNode = nodes.find(
(n) =>
n.type === "link_validation" &&
n.properties.totalLinks === 5 &&
n.properties.brokenLinks === 0,
);
expect(validationNode).toBeDefined();
expect(validationNode?.properties.totalLinks).toBe(5);
expect(validationNode?.properties.healthScore).toBe(100);
});
it("should store validation results with broken links", async () => {
const summary = {
totalLinks: 10,
validLinks: 7,
brokenLinks: 3,
warningLinks: 0,
unknownLinks: 0,
results: [
{
url: "https://broken1.com",
status: "broken" as const,
lastChecked: new Date().toISOString(),
},
{
url: "https://broken2.com",
status: "broken" as const,
lastChecked: new Date().toISOString(),
},
{
url: "https://broken3.com",
status: "broken" as const,
lastChecked: new Date().toISOString(),
},
],
};
await storeLinkValidationInKG("doc-section-2", summary);
const kg = await getKnowledgeGraph();
const edges = await kg.findEdges({
source: "doc-section-2",
type: "has_link_validation",
});
expect(edges.length).toBeGreaterThan(0);
});
it("should create requires_fix edge for broken links", async () => {
const summary = {
totalLinks: 10,
validLinks: 4,
brokenLinks: 6,
warningLinks: 0,
unknownLinks: 0,
results: [
{
url: "https://broken.com",
status: "broken" as const,
lastChecked: new Date().toISOString(),
},
],
};
await storeLinkValidationInKG("doc-section-3", summary);
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
const validationNode = allNodes.find(
(n) => n.type === "link_validation" && n.properties.brokenLinks === 6,
);
expect(validationNode).toBeDefined();
const requiresFixEdges = await kg.findEdges({
source: validationNode!.id,
type: "requires_fix",
});
expect(requiresFixEdges.length).toBeGreaterThan(0);
expect(requiresFixEdges[0].properties.severity).toBe("high"); // > 5 broken links
});
it("should set medium severity for few broken links", async () => {
const summary = {
totalLinks: 10,
validLinks: 8,
brokenLinks: 2,
warningLinks: 0,
unknownLinks: 0,
results: [
{
url: "https://broken.com",
status: "broken" as const,
lastChecked: new Date().toISOString(),
},
],
};
await storeLinkValidationInKG("doc-section-4", summary);
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
const validationNode = allNodes.find(
(n) => n.type === "link_validation" && n.properties.brokenLinks === 2,
);
const requiresFixEdges = await kg.findEdges({
source: validationNode!.id,
type: "requires_fix",
});
expect(requiresFixEdges[0].properties.severity).toBe("medium");
});
it("should calculate health score correctly", async () => {
const summary = {
totalLinks: 20,
validLinks: 15,
brokenLinks: 5,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-5", summary);
const kg = await getKnowledgeGraph();
const nodes = await kg.getAllNodes();
const validationNode = nodes.find(
(n) => n.type === "link_validation" && n.properties.totalLinks === 20,
);
expect(validationNode?.properties.healthScore).toBe(75);
});
it("should handle zero links with 100% health score", async () => {
const summary = {
totalLinks: 0,
validLinks: 0,
brokenLinks: 0,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-6", summary);
const kg = await getKnowledgeGraph();
const nodes = await kg.getAllNodes();
const validationNode = nodes.find(
(n) => n.type === "link_validation" && n.properties.totalLinks === 0,
);
expect(validationNode?.properties.healthScore).toBe(100);
});
});
describe("getLinkValidationHistory", () => {
it("should retrieve validation history", async () => {
const summary1 = {
totalLinks: 5,
validLinks: 5,
brokenLinks: 0,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-7", summary1);
const history = await getLinkValidationHistory("doc-section-7");
expect(history.length).toBeGreaterThan(0);
expect(history[0].type).toBe("link_validation");
});
it("should return empty array for non-existent doc section", async () => {
const history = await getLinkValidationHistory("non-existent");
expect(history).toEqual([]);
});
it("should sort history by newest first", async () => {
// Add two validations with delay to ensure different timestamps
const summary1 = {
totalLinks: 5,
validLinks: 5,
brokenLinks: 0,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-8", summary1);
// Small delay to ensure different timestamp
await new Promise((resolve) => setTimeout(resolve, 10));
const summary2 = {
totalLinks: 6,
validLinks: 6,
brokenLinks: 0,
warningLinks: 0,
unknownLinks: 0,
results: [],
};
await storeLinkValidationInKG("doc-section-8", summary2);
const history = await getLinkValidationHistory("doc-section-8");
expect(history.length).toBeGreaterThan(1);
// First item should be newest
const firstTimestamp = new Date(
history[0].properties.lastValidated,
).getTime();
const secondTimestamp = new Date(
history[1].properties.lastValidated,
).getTime();
expect(firstTimestamp).toBeGreaterThanOrEqual(secondTimestamp);
});
});
});
```
--------------------------------------------------------------------------------
/tests/memory/manager-advanced.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for uncovered branches in Memory Manager
* Covers: getRelated (lines 171-202), export (lines 381-398), import (lines 409-415)
*/
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import { MemoryManager } from "../../src/memory/manager.js";
import { MemoryEntry } from "../../src/memory/storage.js";
describe("MemoryManager - Advanced Features Coverage", () => {
let manager: MemoryManager;
let tempDir: string;
beforeEach(async () => {
tempDir = path.join(
os.tmpdir(),
`manager-advanced-test-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`,
);
await fs.mkdir(tempDir, { recursive: true });
manager = new MemoryManager(tempDir);
await manager.initialize();
});
afterEach(async () => {
try {
await manager.close();
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("getRelated - Tag-based Relationships (lines 189-195)", () => {
it("should find related memories by overlapping tags", async () => {
// Create entries with overlapping tags
const entry1 = await manager.remember(
"analysis",
{ name: "Project A" },
{
projectId: "proj-001",
tags: ["typescript", "react", "frontend"],
},
);
await manager.remember(
"analysis",
{ name: "Project B" },
{
projectId: "proj-002",
tags: ["typescript", "vue", "frontend"],
},
);
await manager.remember(
"analysis",
{ name: "Project C" },
{
projectId: "proj-003",
tags: ["python", "backend"],
},
);
// Get related memories for entry1 (should find project B via overlapping tags)
const related = await manager.getRelated(entry1, 10);
expect(related.length).toBeGreaterThan(0);
// Should include Project B (shares typescript and frontend tags)
const relatedNames = related.map((r) => r.data.name);
expect(relatedNames).toContain("Project B");
// Should not include entry1 itself
expect(relatedNames).not.toContain("Project A");
});
it("should find related memories by same type (lines 182-186)", async () => {
const entry1 = await manager.remember(
"recommendation",
{ ssg: "jekyll" },
{ projectId: "proj-001" },
);
await manager.remember(
"recommendation",
{ ssg: "hugo" },
{ projectId: "proj-002" },
);
await manager.remember(
"analysis",
{ type: "different" },
{ projectId: "proj-003" },
);
const related = await manager.getRelated(entry1, 10);
// Should find the other recommendation, not the analysis
expect(related.length).toBeGreaterThan(0);
const types = related.map((r) => r.type);
expect(types).toContain("recommendation");
});
it("should find related memories by same project (lines 174-179)", async () => {
manager.setContext({ projectId: "shared-project" });
const entry1 = await manager.remember(
"analysis",
{ step: "step1" },
{ projectId: "shared-project" },
);
await manager.remember(
"analysis",
{ step: "step2" },
{ projectId: "shared-project" },
);
await manager.remember(
"analysis",
{ step: "step3" },
{ projectId: "different-project" },
);
const related = await manager.getRelated(entry1, 10);
// Should find step2 from same project
expect(related.length).toBeGreaterThan(0);
const projectIds = related.map((r) => r.metadata.projectId);
expect(projectIds).toContain("shared-project");
});
it("should deduplicate and limit related memories (lines 198-202)", async () => {
const entry1 = await manager.remember(
"analysis",
{ name: "Entry 1" },
{
projectId: "proj-001",
tags: ["tag1", "tag2"],
},
);
// Create many related entries
for (let i = 0; i < 20; i++) {
await manager.remember(
"analysis",
{ name: `Entry ${i + 2}` },
{
projectId: "proj-001",
tags: i < 10 ? ["tag1"] : ["tag2"],
},
);
}
// Request limit of 5
const related = await manager.getRelated(entry1, 5);
// Should be limited to 5 (deduplicated)
expect(related.length).toBeLessThanOrEqual(5);
// Should not include entry1 itself
const names = related.map((r) => r.data.name);
expect(names).not.toContain("Entry 1");
});
it("should handle entry without tags gracefully (line 189)", async () => {
const entryNoTags = await manager.remember(
"analysis",
{ name: "No Tags" },
{ projectId: "proj-001" },
);
await manager.remember(
"analysis",
{ name: "Also No Tags" },
{ projectId: "proj-001" },
);
// Should still find related by project
const related = await manager.getRelated(entryNoTags, 10);
expect(related.length).toBeGreaterThan(0);
});
it("should handle entry with empty tags array (line 189)", async () => {
const entryEmptyTags = await manager.remember(
"analysis",
{ name: "Empty Tags" },
{
projectId: "proj-001",
tags: [],
},
);
await manager.remember(
"analysis",
{ name: "Other Entry" },
{ projectId: "proj-001" },
);
const related = await manager.getRelated(entryEmptyTags, 10);
expect(related.length).toBeGreaterThan(0);
});
});
describe("CSV Export (lines 381-398)", () => {
it("should export memories as CSV format", async () => {
manager.setContext({ projectId: "csv-proj-001" });
await manager.remember(
"analysis",
{ test: "data1" },
{
repository: "github.com/test/repo1",
ssg: "jekyll",
},
);
manager.setContext({ projectId: "csv-proj-002" });
await manager.remember(
"recommendation",
{ test: "data2" },
{
repository: "github.com/test/repo2",
ssg: "hugo",
},
);
// Export as CSV
const csvData = await manager.export("csv");
// Verify CSV structure
expect(csvData).toContain("id,timestamp,type,projectId,repository,ssg");
expect(csvData).toContain("csv-proj-001");
expect(csvData).toContain("csv-proj-002");
expect(csvData).toContain("github.com/test/repo1");
expect(csvData).toContain("github.com/test/repo2");
expect(csvData).toContain("jekyll");
expect(csvData).toContain("hugo");
// Verify rows are comma-separated
const lines = csvData.split("\n").filter((l) => l.trim());
expect(lines.length).toBeGreaterThanOrEqual(3); // header + 2 rows
// Each line should have the same number of commas
const headerCommas = (lines[0].match(/,/g) || []).length;
for (let i = 1; i < lines.length; i++) {
const rowCommas = (lines[i].match(/,/g) || []).length;
expect(rowCommas).toBe(headerCommas);
}
});
it("should export memories for specific project only", async () => {
manager.setContext({ projectId: "project-a" });
await manager.remember("analysis", { project: "A" }, {});
manager.setContext({ projectId: "project-b" });
await manager.remember("analysis", { project: "B" }, {});
// Export only project-a
const csvData = await manager.export("csv", "project-a");
expect(csvData).toContain("project-a");
expect(csvData).not.toContain("project-b");
});
it("should handle missing metadata fields in CSV export (lines 393-395)", async () => {
// Create entry with minimal metadata
await manager.remember("analysis", { test: "minimal" }, {});
const csvData = await manager.export("csv");
// Should have empty fields for missing metadata
const lines = csvData.split("\n");
expect(lines.length).toBeGreaterThan(1);
// Verify header
expect(lines[0]).toContain("id,timestamp,type,projectId,repository,ssg");
// Data row should have appropriate number of commas (empty fields)
const dataRow = lines[1];
const headerCommas = (lines[0].match(/,/g) || []).length;
const dataCommas = (dataRow.match(/,/g) || []).length;
expect(dataCommas).toBe(headerCommas);
});
it("should export as JSON by default", async () => {
await manager.remember(
"analysis",
{ json: "test" },
{ projectId: "json-proj" },
);
const jsonData = await manager.export("json");
const parsed = JSON.parse(jsonData);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBeGreaterThan(0);
expect(parsed[0].data.json).toBe("test");
});
});
describe("CSV Import (lines 409-428)", () => {
it("should import memories from CSV format", async () => {
// Create CSV data
const csvData = `id,timestamp,type,projectId,repository,ssg
mem-001,2024-01-01T00:00:00.000Z,analysis,proj-csv-001,github.com/test/repo1,jekyll
mem-002,2024-01-02T00:00:00.000Z,recommendation,proj-csv-002,github.com/test/repo2,hugo
mem-003,2024-01-03T00:00:00.000Z,deployment,proj-csv-003,github.com/test/repo3,mkdocs`;
const imported = await manager.import(csvData, "csv");
expect(imported).toBe(3);
// Verify entries were imported
const recalled1 = await manager.recall("mem-001");
expect(recalled1).not.toBeNull();
expect(recalled1?.type).toBe("analysis");
expect(recalled1?.metadata.projectId).toBe("proj-csv-001");
expect(recalled1?.metadata.ssg).toBe("jekyll");
const recalled2 = await manager.recall("mem-002");
expect(recalled2).not.toBeNull();
expect(recalled2?.type).toBe("recommendation");
});
it("should skip malformed CSV rows (line 414)", async () => {
// CSV with mismatched column counts
const csvData = `id,timestamp,type,projectId,repository,ssg
mem-001,2024-01-01T00:00:00.000Z,analysis,proj-001,github.com/test/repo,jekyll
mem-002,2024-01-02T00:00:00.000Z,recommendation
mem-003,2024-01-03T00:00:00.000Z,deployment,proj-003,github.com/test/repo3,mkdocs`;
const imported = await manager.import(csvData, "csv");
// Should import 2 (skipping the malformed row)
expect(imported).toBe(2);
// Verify valid entries were imported
const recalled1 = await manager.recall("mem-001");
expect(recalled1).not.toBeNull();
// Malformed entry should not be imported
const recalled2 = await manager.recall("mem-002");
expect(recalled2).toBeNull();
const recalled3 = await manager.recall("mem-003");
expect(recalled3).not.toBeNull();
});
it("should import memories from JSON format", async () => {
const jsonData = JSON.stringify([
{
id: "json-001",
timestamp: "2024-01-01T00:00:00.000Z",
type: "analysis",
data: { test: "json-import" },
metadata: { projectId: "json-proj" },
},
]);
const imported = await manager.import(jsonData, "json");
expect(imported).toBe(1);
const recalled = await manager.recall("json-001");
expect(recalled).not.toBeNull();
expect(recalled?.data.test).toBe("json-import");
});
it("should emit import-complete event (line 437)", async () => {
let eventEmitted = false;
let importedCount = 0;
manager.on("import-complete", (count) => {
eventEmitted = true;
importedCount = count;
});
const jsonData = JSON.stringify([
{
id: "event-001",
timestamp: "2024-01-01T00:00:00.000Z",
type: "analysis",
data: {},
metadata: {},
},
]);
await manager.import(jsonData, "json");
expect(eventEmitted).toBe(true);
expect(importedCount).toBe(1);
});
it("should handle empty CSV import gracefully", async () => {
const csvData = `id,timestamp,type,projectId,repository,ssg`;
const imported = await manager.import(csvData, "csv");
expect(imported).toBe(0);
});
it("should handle empty JSON import gracefully", async () => {
const jsonData = JSON.stringify([]);
const imported = await manager.import(jsonData, "json");
expect(imported).toBe(0);
});
});
describe("Export and Import Round-trip", () => {
it("should maintain data integrity through CSV round-trip", async () => {
// Create test data
manager.setContext({
projectId: "roundtrip-proj",
repository: "github.com/test/roundtrip",
});
const originalEntry = await manager.remember(
"analysis",
{ roundtrip: "test" },
{
ssg: "docusaurus",
},
);
// Export as CSV
const csvData = await manager.export("csv");
// Create new manager and import
const tempDir2 = path.join(
os.tmpdir(),
`manager-roundtrip-${Date.now()}`,
);
await fs.mkdir(tempDir2, { recursive: true });
const manager2 = new MemoryManager(tempDir2);
await manager2.initialize();
const imported = await manager2.import(csvData, "csv");
expect(imported).toBeGreaterThan(0);
// Verify data matches
const recalled = await manager2.recall(originalEntry.id);
expect(recalled).not.toBeNull();
expect(recalled?.type).toBe(originalEntry.type);
expect(recalled?.metadata.projectId).toBe(
originalEntry.metadata.projectId,
);
expect(recalled?.metadata.ssg).toBe(originalEntry.metadata.ssg);
await manager2.close();
await fs.rm(tempDir2, { recursive: true, force: true });
});
it("should maintain data integrity through JSON round-trip", async () => {
// Create test data with complex structure
manager.setContext({ projectId: "json-roundtrip" });
const originalEntry = await manager.remember(
"analysis",
{
complex: "data",
nested: { value: 123 },
array: [1, 2, 3],
},
{
tags: ["tag1", "tag2"],
},
);
// Export as JSON
const jsonData = await manager.export("json");
// Create new manager and import
const tempDir2 = path.join(
os.tmpdir(),
`manager-json-roundtrip-${Date.now()}`,
);
await fs.mkdir(tempDir2, { recursive: true });
const manager2 = new MemoryManager(tempDir2);
await manager2.initialize();
const imported = await manager2.import(jsonData, "json");
expect(imported).toBeGreaterThan(0);
// Verify complex data maintained
const recalled = await manager2.recall(originalEntry.id);
expect(recalled).not.toBeNull();
expect(recalled?.data).toEqual(originalEntry.data);
expect(recalled?.metadata.tags).toEqual(originalEntry.metadata.tags);
await manager2.close();
await fs.rm(tempDir2, { recursive: true, force: true });
});
});
});
```
--------------------------------------------------------------------------------
/src/tools/evaluate-readme-health.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { promises as fs } from "fs";
import path from "path";
import { formatMCPResponse } from "../types/api.js";
// Input validation schema
const EvaluateReadmeHealthSchema = z.object({
readme_path: z.string().min(1, "README path is required"),
project_type: z
.enum([
"community_library",
"enterprise_tool",
"personal_project",
"documentation",
])
.optional()
.default("community_library"),
repository_path: z.string().optional(),
});
// Input type that matches what users actually pass (project_type is optional)
export interface EvaluateReadmeHealthInput {
readme_path: string;
project_type?:
| "community_library"
| "enterprise_tool"
| "personal_project"
| "documentation";
repository_path?: string;
}
// Health score interfaces
interface HealthScoreComponent {
name: string;
score: number;
maxScore: number;
details: HealthCheckDetail[];
}
interface HealthCheckDetail {
check: string;
passed: boolean;
points: number;
maxPoints: number;
recommendation?: string;
}
interface ReadmeHealthReport {
overallScore: number;
maxScore: number;
grade: "A" | "B" | "C" | "D" | "F";
components: {
communityHealth: HealthScoreComponent;
accessibility: HealthScoreComponent;
onboarding: HealthScoreComponent;
contentQuality: HealthScoreComponent;
};
recommendations: string[];
strengths: string[];
criticalIssues: string[];
estimatedImprovementTime: string;
}
export async function evaluateReadmeHealth(input: EvaluateReadmeHealthInput) {
const startTime = Date.now();
try {
// Validate input
const validatedInput = EvaluateReadmeHealthSchema.parse(input);
// Read README file
const readmePath = path.resolve(validatedInput.readme_path);
const readmeContent = await fs.readFile(readmePath, "utf-8");
// Get repository context if available
let repoContext: any = null;
if (validatedInput.repository_path) {
repoContext = await analyzeRepositoryContext(
validatedInput.repository_path,
);
}
// Evaluate all health components
const communityHealth = evaluateCommunityHealth(readmeContent, repoContext);
const accessibility = evaluateAccessibility(readmeContent);
const onboarding = evaluateOnboarding(
readmeContent,
validatedInput.project_type,
);
const contentQuality = evaluateContentQuality(readmeContent);
// Calculate overall score
const totalScore =
communityHealth.score +
accessibility.score +
onboarding.score +
contentQuality.score;
const maxTotalScore =
communityHealth.maxScore +
accessibility.maxScore +
onboarding.maxScore +
contentQuality.maxScore;
const percentage = (totalScore / maxTotalScore) * 100;
// Generate grade
const grade = getGrade(percentage);
// Generate recommendations and insights
const recommendations = generateHealthRecommendations(
[communityHealth, accessibility, onboarding, contentQuality],
"general",
);
const strengths = identifyStrengths([
communityHealth,
accessibility,
onboarding,
contentQuality,
]);
const criticalIssues = identifyCriticalIssues([
communityHealth,
accessibility,
onboarding,
contentQuality,
]);
const report: ReadmeHealthReport = {
overallScore: Math.round(percentage),
maxScore: 100,
grade,
components: {
communityHealth,
accessibility,
onboarding,
contentQuality,
},
recommendations,
strengths,
criticalIssues,
estimatedImprovementTime: estimateImprovementTime(
recommendations.length,
criticalIssues.length,
),
};
const response = {
readmePath: validatedInput.readme_path,
projectType: validatedInput.project_type,
healthReport: report,
summary: generateSummary(report),
nextSteps: generateNextSteps(report),
};
return formatMCPResponse({
success: true,
data: response,
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
return formatMCPResponse({
success: false,
error: {
code: "README_HEALTH_EVALUATION_FAILED",
message: `Failed to evaluate README health: ${error}`,
resolution: "Ensure README path is valid and file is readable",
},
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
});
}
}
function evaluateCommunityHealth(
content: string,
_repoContext: any,
): HealthScoreComponent {
const checks: HealthCheckDetail[] = [
{
check: "Code of Conduct linked",
passed: /code.of.conduct|conduct\.md|\.github\/code_of_conduct/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation:
"Add a link to your Code of Conduct to establish community standards",
},
{
check: "Contributing guidelines visible",
passed: /contributing|contribute\.md|\.github\/contributing/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation:
"Include contributing guidelines to help new contributors get started",
},
{
check: "Issue/PR templates mentioned",
passed:
/issue.template|pull.request.template|\.github\/issue_template|\.github\/pull_request_template/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation:
"Reference issue and PR templates to streamline contributions",
},
{
check: "Security policy linked",
passed: /security\.md|security.policy|\.github\/security/i.test(content),
points: 0,
maxPoints: 5,
recommendation:
"Add a security policy to handle vulnerability reports responsibly",
},
{
check: "Support channels provided",
passed: /support|help|discord|slack|discussions|forum|community/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation: "Provide clear support channels for users seeking help",
},
];
// Award points for passed checks
checks.forEach((check) => {
if (check.passed) {
check.points = check.maxPoints;
}
});
const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
return {
name: "Community Health",
score: totalScore,
maxScore,
details: checks,
};
}
function evaluateAccessibility(content: string): HealthScoreComponent {
const lines = content.split("\n");
const headings = lines.filter((line) => line.trim().startsWith("#"));
const images = content.match(/!\[.*?\]\(.*?\)/g) || [];
const checks: HealthCheckDetail[] = [
{
check: "Scannable structure with proper spacing",
passed: content.includes("\n\n") && lines.length > 10,
points: 0,
maxPoints: 5,
recommendation: "Use proper spacing and breaks to make content scannable",
},
{
check: "Clear heading hierarchy",
passed: headings.length >= 3 && headings.some((h) => h.startsWith("##")),
points: 0,
maxPoints: 5,
recommendation:
"Use proper heading hierarchy (H1, H2, H3) to structure content",
},
{
check: "Alt text for images",
passed:
images.length === 0 || images.every((img) => !img.includes("),
points: 0,
maxPoints: 5,
recommendation:
"Add descriptive alt text for all images for screen readers",
},
{
check: "Inclusive language",
passed: !/\b(guys|blacklist|whitelist|master|slave)\b/i.test(content),
points: 0,
maxPoints: 5,
recommendation:
'Use inclusive language (e.g., "team" instead of "guys", "allowlist/blocklist")',
},
];
// Award points for passed checks
checks.forEach((check) => {
if (check.passed) {
check.points = check.maxPoints;
}
});
const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
return {
name: "Accessibility",
score: totalScore,
maxScore,
details: checks,
};
}
function evaluateOnboarding(
content: string,
_projectType: string,
): HealthScoreComponent {
const checks: HealthCheckDetail[] = [
{
check: "Quick start section",
passed: /quick.start|getting.started|installation|setup/i.test(content),
points: 0,
maxPoints: 5,
recommendation:
"Add a quick start section to help users get up and running fast",
},
{
check: "Prerequisites clearly listed",
passed: /prerequisites|requirements|dependencies|before.you.begin/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation: "Clearly list all prerequisites and system requirements",
},
{
check: "First contribution guide",
passed: /first.contribution|new.contributor|beginner|newcomer/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation:
"Include guidance specifically for first-time contributors",
},
{
check: "Good first issues mentioned",
passed: /good.first.issue|beginner.friendly|easy.pick|help.wanted/i.test(
content,
),
points: 0,
maxPoints: 5,
recommendation: "Mention good first issues or beginner-friendly tasks",
},
];
// Award points for passed checks
checks.forEach((check) => {
if (check.passed) {
check.points = check.maxPoints;
}
});
const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
return {
name: "Onboarding",
score: totalScore,
maxScore,
details: checks,
};
}
function evaluateContentQuality(content: string): HealthScoreComponent {
const wordCount = content.split(/\s+/).length;
const codeBlocks = (content.match(/```/g) || []).length / 2;
const links = (content.match(/\[.*?\]\(.*?\)/g) || []).length;
const checks: HealthCheckDetail[] = [
{
check: "Adequate content length",
passed: wordCount >= 50 && wordCount <= 2000,
points: 0,
maxPoints: 5,
recommendation:
"Maintain optimal README length (50-2000 words) for readability",
},
{
check: "Code examples provided",
passed: codeBlocks >= 2,
points: 0,
maxPoints: 5,
recommendation: "Include practical code examples to demonstrate usage",
},
{
check: "External links present",
passed: links >= 3,
points: 0,
maxPoints: 5,
recommendation:
"Add relevant external links (docs, demos, related projects)",
},
{
check: "Project description clarity",
passed: /## |### /.test(content) && content.length > 500,
points: 0,
maxPoints: 5,
recommendation:
"Provide clear, detailed project description with proper structure",
},
];
// Award points for passed checks
checks.forEach((check) => {
if (check.passed) {
check.points = check.maxPoints;
}
});
const totalScore = checks.reduce((sum, check) => sum + check.points, 0);
const maxScore = checks.reduce((sum, check) => sum + check.maxPoints, 0);
return {
name: "Content Quality",
score: totalScore,
maxScore,
details: checks,
};
}
async function analyzeRepositoryContext(repoPath: string): Promise<any> {
try {
const repoDir = path.resolve(repoPath);
const files = await fs.readdir(repoDir);
return {
hasCodeOfConduct: files.includes("CODE_OF_CONDUCT.md"),
hasContributing: files.includes("CONTRIBUTING.md"),
hasSecurityPolicy: files.includes("SECURITY.md"),
hasGithubDir: files.includes(".github"),
packageJson: files.includes("package.json"),
};
} catch (error) {
return null;
}
}
function getGrade(percentage: number): "A" | "B" | "C" | "D" | "F" {
if (percentage >= 90) return "A";
if (percentage >= 80) return "B";
if (percentage >= 70) return "C";
if (percentage >= 60) return "D";
return "F";
}
function generateHealthRecommendations(
analysis: any[],
_projectType: string,
): string[] {
const recommendations: string[] = [];
analysis.forEach((component: any) => {
component.details.forEach((detail: any) => {
if (detail.points < detail.maxPoints) {
recommendations.push(`${component.name}: ${detail.recommendation}`);
}
});
});
return recommendations.slice(0, 10); // Top 10 recommendations
}
function identifyStrengths(components: HealthScoreComponent[]): string[] {
const strengths: string[] = [];
components.forEach((component) => {
const passedChecks = component.details.filter((detail) => detail.passed);
if (passedChecks.length > component.details.length / 2) {
strengths.push(
`Strong ${component.name.toLowerCase()}: ${passedChecks
.map((c) => c.check.toLowerCase())
.join(", ")}`,
);
}
});
return strengths;
}
function identifyCriticalIssues(components: HealthScoreComponent[]): string[] {
const critical: string[] = [];
components.forEach((component) => {
if (component.score < component.maxScore * 0.3) {
// Less than 30% score
critical.push(
`Critical: Poor ${component.name.toLowerCase()} (${component.score}/${
component.maxScore
} points)`,
);
}
});
return critical;
}
function estimateImprovementTime(
recommendationCount: number,
criticalCount: number,
): string {
const baseTime = recommendationCount * 15; // 15 minutes per recommendation
const criticalTime = criticalCount * 30; // 30 minutes per critical issue
const totalMinutes = baseTime + criticalTime;
if (totalMinutes < 60) return `${totalMinutes} minutes`;
if (totalMinutes < 480) return `${Math.round(totalMinutes / 60)} hours`;
return `${Math.round(totalMinutes / 480)} days`;
}
function generateSummary(report: ReadmeHealthReport): string {
const { overallScore, grade, components } = report;
const componentScores = Object.values(components)
.map((c) => `${c.name}: ${c.score}/${c.maxScore}`)
.join(", ");
return `README Health Score: ${overallScore}/100 (Grade ${grade}). Component breakdown: ${componentScores}. ${report.criticalIssues.length} critical issues identified.`;
}
function generateNextSteps(report: ReadmeHealthReport): string[] {
const steps: string[] = [];
if (report.criticalIssues.length > 0) {
steps.push(
"Address critical issues first to establish baseline community health",
);
}
if (report.recommendations.length > 0) {
steps.push(
`Implement top ${Math.min(
3,
report.recommendations.length,
)} recommendations for quick wins`,
);
}
if (report.overallScore < 85) {
steps.push("Target 85+ health score for optimal community engagement");
}
steps.push("Re-evaluate after improvements to track progress");
return steps;
}
```
--------------------------------------------------------------------------------
/tests/tools/analyze-deployments.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for Phase 2.4: Deployment Analytics and Insights
* Tests the analyze_deployments tool with comprehensive pattern analysis
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
initializeKnowledgeGraph,
getKnowledgeGraph,
createOrUpdateProject,
trackDeployment,
} from "../../src/memory/kg-integration.js";
import { analyzeDeployments } from "../../src/tools/analyze-deployments.js";
describe("analyzeDeployments (Phase 2.4)", () => {
let testDir: string;
let originalEnv: string | undefined;
beforeEach(async () => {
// Create temporary test directory
testDir = join(tmpdir(), `analyze-deployments-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
// Set environment variable for storage
originalEnv = process.env.DOCUMCP_STORAGE_DIR;
process.env.DOCUMCP_STORAGE_DIR = testDir;
// Initialize KG
await initializeKnowledgeGraph(testDir);
});
afterEach(async () => {
// Restore environment
if (originalEnv) {
process.env.DOCUMCP_STORAGE_DIR = originalEnv;
} else {
delete process.env.DOCUMCP_STORAGE_DIR;
}
// Clean up test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch (error) {
console.warn("Failed to clean up test directory:", error);
}
});
/**
* Helper function to create sample deployment data
*/
const createSampleDeployments = async () => {
const timestamp = new Date().toISOString();
// Create 3 projects
const project1 = await createOrUpdateProject({
id: "project1",
timestamp,
path: "/test/project1",
projectName: "Docusaurus Site",
structure: {
totalFiles: 50,
languages: { typescript: 30, javascript: 20 },
hasTests: true,
hasCI: true,
hasDocs: true,
},
});
const project2 = await createOrUpdateProject({
id: "project2",
timestamp,
path: "/test/project2",
projectName: "Hugo Blog",
structure: {
totalFiles: 30,
languages: { go: 15, html: 15 },
hasTests: false,
hasCI: true,
hasDocs: true,
},
});
const project3 = await createOrUpdateProject({
id: "project3",
timestamp,
path: "/test/project3",
projectName: "MkDocs Docs",
structure: {
totalFiles: 40,
languages: { python: 25, markdown: 15 },
hasTests: true,
hasCI: true,
hasDocs: true,
},
});
// Track successful deployments
await trackDeployment(project1.id, "docusaurus", true, {
buildTime: 25000,
});
await trackDeployment(project1.id, "docusaurus", true, {
buildTime: 23000,
});
await trackDeployment(project2.id, "hugo", true, { buildTime: 15000 });
await trackDeployment(project2.id, "hugo", true, { buildTime: 14000 });
await trackDeployment(project2.id, "hugo", true, { buildTime: 16000 });
await trackDeployment(project3.id, "mkdocs", true, { buildTime: 30000 });
await trackDeployment(project3.id, "mkdocs", false, {
errorMessage: "Build failed",
});
return { project1, project2, project3 };
};
describe("Full Report Analysis", () => {
it("should generate comprehensive analytics report with no data", async () => {
const result = await analyzeDeployments({});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
expect(data.summary).toBeDefined();
expect(data.summary.totalProjects).toBe(0);
expect(data.summary.totalDeployments).toBe(0);
expect(data.patterns).toEqual([]);
// With 0 deployments, we get a warning insight about low success rate
expect(Array.isArray(data.insights)).toBe(true);
expect(data.recommendations).toBeDefined();
});
it("should generate comprehensive analytics report with sample data", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
// Verify summary
expect(data.summary).toBeDefined();
expect(data.summary.totalProjects).toBe(3);
// Each project has 1 configuration node, so 3 total deployments tracked
expect(data.summary.totalDeployments).toBeGreaterThanOrEqual(3);
expect(data.summary.overallSuccessRate).toBeGreaterThan(0);
expect(data.summary.mostUsedSSG).toBeDefined();
// Verify patterns
expect(data.patterns).toBeDefined();
expect(data.patterns.length).toBeGreaterThan(0);
expect(data.patterns[0]).toHaveProperty("ssg");
expect(data.patterns[0]).toHaveProperty("totalDeployments");
expect(data.patterns[0]).toHaveProperty("successRate");
// Verify insights and recommendations
expect(data.insights).toBeDefined();
expect(data.recommendations).toBeDefined();
});
it("should include insights about high success rates", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Should have success insights for docusaurus and hugo
const successInsights = data.insights.filter(
(i: any) => i.type === "success",
);
expect(successInsights.length).toBeGreaterThan(0);
});
});
describe("SSG Statistics Analysis", () => {
it("should return error for non-existent SSG", async () => {
const result = await analyzeDeployments({
analysisType: "ssg_stats",
ssg: "nonexistent",
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Should return error response when SSG has no data
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
});
it("should return statistics for specific SSG", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "ssg_stats",
ssg: "docusaurus",
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
expect(data.ssg).toBe("docusaurus");
// project1 has 2 deployments with docusaurus
expect(data.totalDeployments).toBeGreaterThanOrEqual(1);
expect(data.successfulDeployments).toBeGreaterThanOrEqual(1);
expect(data.successRate).toBeGreaterThan(0);
expect(data.averageBuildTime).toBeDefined();
expect(data.projectCount).toBeGreaterThan(0);
});
it("should calculate average build time correctly", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "ssg_stats",
ssg: "hugo",
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.averageBuildTime).toBeDefined();
// Hugo has 3 deployments with build times
expect(data.averageBuildTime).toBeGreaterThan(0);
expect(data.averageBuildTime).toBeLessThan(20000);
});
it("should show success rate less than 100% for failed deployments", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "ssg_stats",
ssg: "mkdocs",
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.totalDeployments).toBeGreaterThanOrEqual(1);
expect(data.failedDeployments).toBeGreaterThanOrEqual(1);
expect(data.successRate).toBeLessThan(1.0);
});
});
describe("SSG Comparison Analysis", () => {
it("should fail without enough SSGs", async () => {
const result = await analyzeDeployments({
analysisType: "compare",
ssgs: ["docusaurus"],
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
// Should be an error response
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
expect(data.error.code).toBe("ANALYTICS_FAILED");
});
it("should compare multiple SSGs by success rate", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "compare",
ssgs: ["docusaurus", "hugo", "mkdocs"],
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThan(0);
// Should be sorted by success rate (descending)
for (let i = 0; i < data.length - 1; i++) {
expect(data[i].pattern.successRate).toBeGreaterThanOrEqual(
data[i + 1].pattern.successRate,
);
}
});
it("should include only SSGs with deployment data", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "compare",
ssgs: ["docusaurus", "nonexistent", "hugo"],
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Should only include docusaurus and hugo
expect(data.length).toBe(2);
const ssgs = data.map((d: any) => d.ssg);
expect(ssgs).toContain("docusaurus");
expect(ssgs).toContain("hugo");
expect(ssgs).not.toContain("nonexistent");
});
});
describe("Health Score Analysis", () => {
it("should calculate health score with no data", async () => {
const result = await analyzeDeployments({
analysisType: "health",
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
expect(data.score).toBeDefined();
expect(data.score).toBeGreaterThanOrEqual(0);
expect(data.score).toBeLessThanOrEqual(100);
expect(data.factors).toBeDefined();
expect(Array.isArray(data.factors)).toBe(true);
expect(data.factors.length).toBe(4); // 4 factors
});
it("should calculate health score with sample data", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "health",
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.score).toBeGreaterThan(0);
expect(data.factors.length).toBe(4);
// Check all factors present
const factorNames = data.factors.map((f: any) => f.name);
expect(factorNames).toContain("Overall Success Rate");
expect(factorNames).toContain("Active Projects");
expect(factorNames).toContain("Deployment Activity");
expect(factorNames).toContain("SSG Diversity");
// Each factor should have impact and status
data.factors.forEach((factor: any) => {
expect(factor.impact).toBeDefined();
expect(factor.status).toMatch(/^(good|warning|critical)$/);
});
});
it("should have good health with high success rate", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "health",
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Should have decent health with our sample data
expect(data.score).toBeGreaterThan(30);
const successRateFactor = data.factors.find(
(f: any) => f.name === "Overall Success Rate",
);
expect(successRateFactor.status).toMatch(/^(good|warning)$/);
});
});
describe("Trend Analysis", () => {
it("should analyze trends with default period", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "trends",
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
expect(Array.isArray(data)).toBe(true);
// Trends are grouped by time periods
});
it("should analyze trends with custom period", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "trends",
periodDays: 7,
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(Array.isArray(data)).toBe(true);
});
});
describe("Error Handling", () => {
it("should handle missing SSG parameter for ssg_stats", async () => {
const result = await analyzeDeployments({
analysisType: "ssg_stats",
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
expect(data.error.code).toBe("ANALYTICS_FAILED");
expect(data.error.message).toContain("SSG name required");
});
it("should handle invalid analysis type gracefully", async () => {
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
expect(content.type).toBe("text");
// Should not throw, should return valid response
});
});
describe("Recommendations Generation", () => {
it("should generate recommendations based on patterns", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.recommendations).toBeDefined();
expect(Array.isArray(data.recommendations)).toBe(true);
expect(data.recommendations.length).toBeGreaterThan(0);
});
it("should recommend best performing SSG", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Should have recommendations
expect(data.recommendations.length).toBeGreaterThan(0);
// At least one recommendation should mention an SSG or general advice
const allText = data.recommendations.join(" ").toLowerCase();
expect(allText.length).toBeGreaterThan(0);
});
});
describe("Build Time Analysis", () => {
it("should identify fast builds in insights", async () => {
await createSampleDeployments();
const result = await analyzeDeployments({
analysisType: "full_report",
});
const content = result.content[0];
const data = JSON.parse(content.text);
// Hugo has ~15s builds, should be identified as fast
const fastBuildInsights = data.insights.filter(
(i: any) => i.title && i.title.includes("Fast Builds"),
);
expect(fastBuildInsights.length).toBeGreaterThan(0);
});
});
});
```
--------------------------------------------------------------------------------
/tests/call-graph-builder.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for Call Graph Builder (Issue #72)
*/
import {
ASTAnalyzer,
CallGraph,
CallGraphNode,
ConditionalPath,
ExceptionPath,
CallGraphOptions,
} from "../src/utils/ast-analyzer.js";
import { promises as fs } from "fs";
import path from "path";
import os from "os";
describe("Call Graph Builder", () => {
let analyzer: ASTAnalyzer;
let tempDir: string;
beforeAll(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "call-graph-test-"));
analyzer = new ASTAnalyzer();
await analyzer.initialize();
});
afterAll(async () => {
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe("buildCallGraph", () => {
it("should build call graph for a simple function", async () => {
const code = `
export function main() {
helper();
}
function helper() {
return "helped";
}
`;
await fs.writeFile(path.join(tempDir, "simple.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir);
expect(graph.entryPoint).toBe("main");
expect(graph.root.function.name).toBe("main");
expect(graph.root.calls.length).toBeGreaterThanOrEqual(1);
expect(graph.root.calls[0]?.function.name).toBe("helper");
});
it("should handle nested function calls", async () => {
const code = `
export function outer() {
middle();
}
function middle() {
inner();
}
function inner() {
return 42;
}
`;
await fs.writeFile(path.join(tempDir, "nested.ts"), code);
const graph = await analyzer.buildCallGraph("outer", tempDir, {
maxDepth: 5,
});
expect(graph.entryPoint).toBe("outer");
expect(graph.maxDepthReached).toBeGreaterThanOrEqual(2);
// Find middle call
const middleCall = graph.root.calls.find(
(c) => c.function.name === "middle",
);
expect(middleCall).toBeDefined();
// Find inner call within middle
if (middleCall) {
const innerCall = middleCall.calls.find(
(c) => c.function.name === "inner",
);
expect(innerCall).toBeDefined();
}
});
it("should respect maxDepth option", async () => {
const code = `
export function level1() {
level2();
}
function level2() {
level3();
}
function level3() {
level4();
}
function level4() {
return "deep";
}
`;
await fs.writeFile(path.join(tempDir, "deep.ts"), code);
const graph = await analyzer.buildCallGraph("level1", tempDir, {
maxDepth: 2,
});
expect(graph.maxDepthReached).toBeLessThanOrEqual(2);
// level3 and level4 should not be fully traversed
});
it("should detect circular references", async () => {
const code = `
export function funcA() {
funcB();
}
function funcB() {
funcA();
}
`;
await fs.writeFile(path.join(tempDir, "circular.ts"), code);
const graph = await analyzer.buildCallGraph("funcA", tempDir);
expect(graph.circularReferences.length).toBeGreaterThanOrEqual(0);
});
it("should return empty graph for non-existent function", async () => {
const graph = await analyzer.buildCallGraph(
"nonExistentFunction",
tempDir,
);
expect(graph.entryPoint).toBe("nonExistentFunction");
expect(graph.root.isExternal).toBe(true);
expect(graph.unresolvedCalls.length).toBeGreaterThan(0);
});
});
describe("Conditional branch extraction", () => {
it("should extract if/else branches", async () => {
const code = `
export function withCondition(flag: boolean) {
if (flag) {
trueHandler();
} else {
falseHandler();
}
}
function trueHandler() {
return true;
}
function falseHandler() {
return false;
}
`;
await fs.writeFile(path.join(tempDir, "conditional.ts"), code);
const graph = await analyzer.buildCallGraph("withCondition", tempDir, {
extractConditionals: true,
});
expect(graph.root.conditionalBranches.length).toBeGreaterThanOrEqual(1);
const ifBranch = graph.root.conditionalBranches.find(
(c) => c.type === "if",
);
expect(ifBranch).toBeDefined();
expect(ifBranch?.condition).toContain("flag");
});
it("should extract switch statement branches", async () => {
const code = `
export function handleStatus(status: string) {
switch (status) {
case "success":
handleSuccess();
break;
case "error":
handleError();
break;
default:
handleDefault();
}
}
function handleSuccess() {}
function handleError() {}
function handleDefault() {}
`;
await fs.writeFile(path.join(tempDir, "switch.ts"), code);
const graph = await analyzer.buildCallGraph("handleStatus", tempDir, {
extractConditionals: true,
});
const switchCases = graph.root.conditionalBranches.filter(
(c) => c.type === "switch-case",
);
expect(switchCases.length).toBeGreaterThanOrEqual(1);
});
it("should extract ternary operators", async () => {
const code = `
export function ternaryExample(condition: boolean) {
const result = condition ? getValue() : getDefault();
return result;
}
function getValue() { return 1; }
function getDefault() { return 0; }
`;
await fs.writeFile(path.join(tempDir, "ternary.ts"), code);
const graph = await analyzer.buildCallGraph("ternaryExample", tempDir, {
extractConditionals: true,
});
const ternary = graph.root.conditionalBranches.find(
(c) => c.type === "ternary",
);
expect(ternary).toBeDefined();
});
it("should skip conditional extraction when disabled", async () => {
const code = `
export function withCondition(flag: boolean) {
if (flag) {
trueHandler();
}
}
function trueHandler() {}
`;
await fs.writeFile(path.join(tempDir, "no-conditionals.ts"), code);
const graph = await analyzer.buildCallGraph("withCondition", tempDir, {
extractConditionals: false,
});
expect(graph.root.conditionalBranches.length).toBe(0);
});
});
describe("Exception path identification", () => {
it("should detect throw statements", async () => {
const code = `
export function throwingFunction() {
throw new Error("Something went wrong");
}
`;
await fs.writeFile(path.join(tempDir, "throwing.ts"), code);
const graph = await analyzer.buildCallGraph("throwingFunction", tempDir, {
trackExceptions: true,
});
expect(graph.root.exceptions.length).toBeGreaterThanOrEqual(1);
const exception = graph.root.exceptions[0];
expect(exception?.exceptionType).toBe("Error");
expect(exception?.isCaught).toBe(false);
});
it("should detect caught exceptions", async () => {
const code = `
export function caughtException() {
try {
throw new Error("Will be caught");
} catch (e) {
console.log(e);
}
}
`;
await fs.writeFile(path.join(tempDir, "caught.ts"), code);
const graph = await analyzer.buildCallGraph("caughtException", tempDir, {
trackExceptions: true,
});
const caughtExceptions = graph.root.exceptions.filter((e) => e.isCaught);
expect(caughtExceptions.length).toBeGreaterThanOrEqual(1);
});
it("should detect custom exception types", async () => {
const code = `
class CustomError extends Error {}
export function customException() {
throw new CustomError("Custom error");
}
`;
await fs.writeFile(path.join(tempDir, "custom-error.ts"), code);
const graph = await analyzer.buildCallGraph("customException", tempDir, {
trackExceptions: true,
});
const customEx = graph.root.exceptions.find(
(e) => e.exceptionType === "CustomError",
);
expect(customEx).toBeDefined();
});
it("should skip exception tracking when disabled", async () => {
const code = `
export function throwingFunction() {
throw new Error("Something went wrong");
}
`;
await fs.writeFile(path.join(tempDir, "no-exceptions.ts"), code);
const graph = await analyzer.buildCallGraph("throwingFunction", tempDir, {
trackExceptions: false,
});
expect(graph.root.exceptions.length).toBe(0);
});
});
describe("Cross-file import resolution", () => {
it("should resolve same-directory imports", async () => {
// Create main file
const mainCode = `
import { helper } from "./helper.js";
export function main() {
helper();
}
`;
// Create helper file
const helperCode = `
export function helper() {
return "helped";
}
`;
const subDir = path.join(tempDir, "cross-file");
await fs.mkdir(subDir, { recursive: true });
await fs.writeFile(path.join(subDir, "main.ts"), mainCode);
await fs.writeFile(path.join(subDir, "helper.ts"), helperCode);
const graph = await analyzer.buildCallGraph("main", subDir, {
resolveImports: true,
});
// Should have found the helper function
expect(graph.analyzedFiles.length).toBeGreaterThanOrEqual(1);
});
it("should track unresolved external calls", async () => {
const code = `
import { externalFunction } from "external-package";
export function main() {
externalFunction();
unknownFunction();
}
`;
await fs.writeFile(path.join(tempDir, "external.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir);
// External package calls should be unresolved
expect(graph.unresolvedCalls.length).toBeGreaterThanOrEqual(1);
});
it("should skip import resolution when disabled", async () => {
const code = `
import { helper } from "./helper.js";
export function main() {
helper();
}
`;
await fs.writeFile(path.join(tempDir, "no-resolve.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir, {
resolveImports: false,
});
// Only the main file should be analyzed
expect(graph.analyzedFiles.length).toBeLessThanOrEqual(1);
});
});
describe("Class method handling", () => {
it("should handle class method calls", async () => {
const code = `
export class MyClass {
public methodA() {
this.methodB();
}
private methodB() {
return "B";
}
}
`;
await fs.writeFile(path.join(tempDir, "class-methods.ts"), code);
const graph = await analyzer.buildCallGraph("methodA", tempDir);
// Should find methodA as entry
expect(graph.entryPoint).toBe("methodA");
});
});
describe("CallGraph structure", () => {
it("should have correct graph structure", async () => {
const code = `
export function main() {
helper();
}
function helper() {}
`;
await fs.writeFile(path.join(tempDir, "structure.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir);
// Verify graph properties
expect(graph).toHaveProperty("entryPoint");
expect(graph).toHaveProperty("root");
expect(graph).toHaveProperty("allFunctions");
expect(graph).toHaveProperty("maxDepthReached");
expect(graph).toHaveProperty("analyzedFiles");
expect(graph).toHaveProperty("circularReferences");
expect(graph).toHaveProperty("unresolvedCalls");
expect(graph).toHaveProperty("buildTime");
// Verify allFunctions is a Map
expect(graph.allFunctions).toBeInstanceOf(Map);
});
it("should have correct node structure", async () => {
const code = `
export function main() {
helper();
}
function helper() {}
`;
await fs.writeFile(path.join(tempDir, "node-structure.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir);
// Verify node properties
const node = graph.root;
expect(node).toHaveProperty("function");
expect(node).toHaveProperty("location");
expect(node).toHaveProperty("calls");
expect(node).toHaveProperty("conditionalBranches");
expect(node).toHaveProperty("exceptions");
expect(node).toHaveProperty("depth");
expect(node).toHaveProperty("truncated");
expect(node).toHaveProperty("isExternal");
// Verify location structure
expect(node.location).toHaveProperty("file");
expect(node.location).toHaveProperty("line");
});
});
describe("Edge cases", () => {
it("should handle empty function body", async () => {
const code = `
export function emptyFunction() {}
`;
await fs.writeFile(path.join(tempDir, "empty.ts"), code);
const graph = await analyzer.buildCallGraph("emptyFunction", tempDir);
expect(graph.root.function.name).toBe("emptyFunction");
expect(graph.root.calls.length).toBe(0);
});
it("should handle async functions", async () => {
const code = `
export async function asyncMain() {
await asyncHelper();
}
async function asyncHelper() {
return Promise.resolve(42);
}
`;
await fs.writeFile(path.join(tempDir, "async.ts"), code);
const graph = await analyzer.buildCallGraph("asyncMain", tempDir);
expect(graph.root.function.name).toBe("asyncMain");
expect(graph.root.function.isAsync).toBe(true);
});
it("should handle arrow functions", async () => {
const code = `
export const arrowMain = () => {
arrowHelper();
};
const arrowHelper = () => "helped";
`;
await fs.writeFile(path.join(tempDir, "arrow.ts"), code);
const graph = await analyzer.buildCallGraph("arrowMain", tempDir);
expect(graph.root.function.name).toBe("arrowMain");
});
it("should handle functions with complex parameters", async () => {
const code = `
export function complexParams(
required: string,
optional?: number,
defaultVal = "default",
...rest: string[]
) {
helper();
}
function helper() {}
`;
await fs.writeFile(path.join(tempDir, "complex-params.ts"), code);
const graph = await analyzer.buildCallGraph("complexParams", tempDir);
expect(graph.root.function.parameters.length).toBeGreaterThanOrEqual(1);
});
it("should handle deeply nested conditionals", async () => {
const code = `
export function nested(a: boolean, b: boolean, c: boolean) {
if (a) {
if (b) {
if (c) {
deepFunction();
}
}
}
}
function deepFunction() {}
`;
await fs.writeFile(path.join(tempDir, "nested-conditionals.ts"), code);
const graph = await analyzer.buildCallGraph("nested", tempDir, {
extractConditionals: true,
});
// Should have at least one conditional
expect(graph.root.conditionalBranches.length).toBeGreaterThanOrEqual(1);
});
});
describe("Options validation", () => {
it("should use default options when not provided", async () => {
const code = `
export function main() {}
`;
await fs.writeFile(path.join(tempDir, "defaults.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir);
// Should complete without errors using defaults
expect(graph.entryPoint).toBe("main");
});
it("should handle custom extensions option", async () => {
const code = `
export function main() {}
`;
await fs.writeFile(path.join(tempDir, "custom.ts"), code);
const graph = await analyzer.buildCallGraph("main", tempDir, {
extensions: [".ts"],
});
expect(graph.entryPoint).toBe("main");
});
});
});
```
--------------------------------------------------------------------------------
/tests/memory/kg-code-integration.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for Knowledge Graph Code Integration
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import path from "path";
import { tmpdir } from "os";
import {
createCodeFileEntities,
createDocumentationEntities,
linkCodeToDocs,
} from "../../src/memory/kg-code-integration.js";
import { ExtractedContent } from "../../src/utils/content-extractor.js";
import {
initializeKnowledgeGraph,
getKnowledgeGraph,
} from "../../src/memory/kg-integration.js";
describe("KG Code Integration", () => {
let testDir: string;
let projectId: string;
beforeEach(async () => {
// Create temporary directory for test files
testDir = path.join(tmpdir(), `documcp-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
projectId = `project:test_${Date.now()}`;
// Initialize KG with test storage
const storageDir = path.join(testDir, ".documcp/memory");
await initializeKnowledgeGraph(storageDir);
});
afterEach(async () => {
// Cleanup
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe("createCodeFileEntities", () => {
it("should create code file entities from TypeScript files", async () => {
// Create a test TypeScript file
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
const tsContent = `
export class UserService {
async getUser(id: string) {
return { id, name: "Test User" };
}
async createUser(data: any) {
return { ...data, id: "123" };
}
}
export async function validateUser(user: any) {
return user.name && user.id;
}
`;
await fs.writeFile(path.join(srcDir, "user.ts"), tsContent, "utf-8");
// Create entities
const entities = await createCodeFileEntities(projectId, testDir);
// Assertions
expect(entities.length).toBe(1);
expect(entities[0].type).toBe("code_file");
expect(entities[0].properties.language).toBe("typescript");
expect(entities[0].properties.path).toBe("src/user.ts");
expect(entities[0].properties.classes).toContain("UserService");
expect(entities[0].properties.functions).toContain("validateUser");
expect(entities[0].properties.contentHash).toBeDefined();
expect(entities[0].properties.linesOfCode).toBeGreaterThan(0);
});
it("should create code file entities from Python files", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
const pyContent = `
class Database:
def connect(self):
pass
def query(self, sql):
return []
def initialize_db():
return Database()
`;
await fs.writeFile(path.join(srcDir, "database.py"), pyContent, "utf-8");
const entities = await createCodeFileEntities(projectId, testDir);
expect(entities.length).toBe(1);
expect(entities[0].properties.language).toBe("python");
expect(entities[0].properties.classes).toContain("Database");
expect(entities[0].properties.functions).toContain("initialize_db");
});
it("should handle nested directories", async () => {
const nestedDir = path.join(testDir, "src", "services", "auth");
await fs.mkdir(nestedDir, { recursive: true });
await fs.writeFile(
path.join(nestedDir, "login.ts"),
"export function login() {}",
"utf-8",
);
const entities = await createCodeFileEntities(projectId, testDir);
expect(entities.length).toBe(1);
expect(entities[0].properties.path).toBe("src/services/auth/login.ts");
});
it("should skip non-code files", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(path.join(srcDir, "README.md"), "# Readme", "utf-8");
await fs.writeFile(path.join(srcDir, "config.json"), "{}", "utf-8");
const entities = await createCodeFileEntities(projectId, testDir);
expect(entities.length).toBe(0);
});
it("should estimate complexity correctly", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
// Small file - low complexity
const smallFile = "export function simple() { return 1; }";
await fs.writeFile(path.join(srcDir, "small.ts"), smallFile, "utf-8");
// Large file - high complexity
const largeFile = Array(200)
.fill("function test() { return 1; }")
.join("\n");
await fs.writeFile(path.join(srcDir, "large.ts"), largeFile, "utf-8");
const entities = await createCodeFileEntities(projectId, testDir);
const smallEntity = entities.find((e) =>
e.properties.path.includes("small.ts"),
);
const largeEntity = entities.find((e) =>
e.properties.path.includes("large.ts"),
);
expect(smallEntity?.properties.complexity).toBe("low");
expect(largeEntity?.properties.complexity).toBe("high");
});
it("should create relationships with project", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "test.ts"),
"export function test() {}",
"utf-8",
);
await createCodeFileEntities(projectId, testDir);
const kg = await getKnowledgeGraph();
const edges = await kg.findEdges({ source: projectId });
expect(edges.some((e) => e.type === "depends_on")).toBe(true);
});
});
describe("createDocumentationEntities", () => {
it("should create documentation section entities from README", async () => {
const extractedContent: ExtractedContent = {
readme: {
content: "# My Project\n\nThis is a test project.",
sections: [
{
title: "My Project",
content: "This is a test project.",
level: 1,
},
{ title: "Installation", content: "npm install", level: 2 },
],
},
existingDocs: [],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const entities = await createDocumentationEntities(
projectId,
extractedContent,
);
expect(entities.length).toBe(2);
expect(entities[0].type).toBe("documentation_section");
expect(entities[0].properties.sectionTitle).toBe("My Project");
expect(entities[0].properties.contentHash).toBeDefined();
expect(entities[0].properties.category).toBe("reference");
});
it("should categorize documentation correctly", async () => {
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/tutorials/getting-started.md",
title: "Getting Started",
content: "# Tutorial",
category: "tutorial",
},
{
path: "docs/how-to/deploy.md",
title: "Deploy Guide",
content: "# How to Deploy",
category: "how-to",
},
{
path: "docs/api/reference.md",
title: "API Reference",
content: "# API",
category: "reference",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const entities = await createDocumentationEntities(
projectId,
extractedContent,
);
expect(entities.length).toBe(3);
expect(
entities.find((e) => e.properties.category === "tutorial"),
).toBeDefined();
expect(
entities.find((e) => e.properties.category === "how-to"),
).toBeDefined();
expect(
entities.find((e) => e.properties.category === "reference"),
).toBeDefined();
});
it("should extract code references from content", async () => {
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/guide.md",
title: "Guide",
content:
"Call `getUserById()` from `src/user.ts` using `UserService` class",
category: "how-to",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const entities = await createDocumentationEntities(
projectId,
extractedContent,
);
expect(entities[0].properties.referencedCodeFiles).toContain(
"src/user.ts",
);
expect(entities[0].properties.referencedFunctions).toContain(
"getUserById",
);
expect(entities[0].properties.referencedClasses).toContain("UserService");
});
it("should detect code examples in documentation", async () => {
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/example.md",
title: "Example",
content: "# Example\n\n```typescript\nconst x = 1;\n```",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const entities = await createDocumentationEntities(
projectId,
extractedContent,
);
expect(entities[0].properties.hasCodeExamples).toBe(true);
expect(entities[0].properties.effectivenessScore).toBeGreaterThan(0.5);
});
it("should process ADRs as explanation category", async () => {
const extractedContent: ExtractedContent = {
existingDocs: [],
adrs: [
{
number: "001",
title: "Use TypeScript",
status: "Accepted",
content: "We will use TypeScript for type safety",
decision: "Use TypeScript",
consequences: "Better IDE support",
},
],
codeExamples: [],
apiDocs: [],
};
const entities = await createDocumentationEntities(
projectId,
extractedContent,
);
expect(entities.length).toBe(1);
expect(entities[0].properties.category).toBe("explanation");
expect(entities[0].properties.sectionTitle).toBe("Use TypeScript");
});
});
describe("linkCodeToDocs", () => {
it("should create references edges when docs reference code", async () => {
// Create code entity
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "user.ts"),
"export function getUser() {}",
"utf-8",
);
const codeFiles = await createCodeFileEntities(projectId, testDir);
// Create doc entity that references the code
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/api.md",
title: "API",
content: "Use `getUser()` from `src/user.ts`",
category: "reference",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const docSections = await createDocumentationEntities(
projectId,
extractedContent,
);
// Link them
const edges = await linkCodeToDocs(codeFiles, docSections);
// Should create references edge (doc -> code)
const referencesEdge = edges.find((e) => e.type === "references");
expect(referencesEdge).toBeDefined();
expect(referencesEdge?.source).toBe(docSections[0].id);
expect(referencesEdge?.target).toBe(codeFiles[0].id);
expect(referencesEdge?.properties.referenceType).toBe("api-reference");
// Should create documents edge (code -> doc)
const documentsEdge = edges.find((e) => e.type === "documents");
expect(documentsEdge).toBeDefined();
expect(documentsEdge?.source).toBe(codeFiles[0].id);
expect(documentsEdge?.target).toBe(docSections[0].id);
});
it("should detect outdated documentation", async () => {
// Create code entity with recent modification
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "user.ts"),
"export function getUser() {}",
"utf-8",
);
const codeFiles = await createCodeFileEntities(projectId, testDir);
// Simulate old documentation (modify lastUpdated)
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/api.md",
title: "API",
content: "Use `getUser()` from `src/user.ts`",
category: "reference",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const docSections = await createDocumentationEntities(
projectId,
extractedContent,
);
// Manually set old timestamp on doc
docSections[0].properties.lastUpdated = new Date(
Date.now() - 86400000,
).toISOString();
const edges = await linkCodeToDocs(codeFiles, docSections);
// Should create outdated_for edge
const outdatedEdge = edges.find((e) => e.type === "outdated_for");
expect(outdatedEdge).toBeDefined();
expect(outdatedEdge?.properties.severity).toBe("medium");
});
it("should determine coverage based on referenced functions", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
// Code with 3 functions
await fs.writeFile(
path.join(srcDir, "user.ts"),
`
export function getUser() {}
export function createUser() {}
export function deleteUser() {}
`,
"utf-8",
);
const codeFiles = await createCodeFileEntities(projectId, testDir);
// Doc that only references 2 functions (66% coverage)
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/api.md",
title: "API",
content: "Use `getUser()` and `createUser()` from `src/user.ts`",
category: "reference",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const docSections = await createDocumentationEntities(
projectId,
extractedContent,
);
const edges = await linkCodeToDocs(codeFiles, docSections);
const documentsEdge = edges.find((e) => e.type === "documents");
expect(documentsEdge?.properties.coverage).toBe("complete"); // >= 50%
});
it("should handle documentation with no code references", async () => {
const srcDir = path.join(testDir, "src");
await fs.mkdir(srcDir, { recursive: true });
await fs.writeFile(
path.join(srcDir, "user.ts"),
"export function getUser() {}",
"utf-8",
);
const codeFiles = await createCodeFileEntities(projectId, testDir);
// Doc with no code references
const extractedContent: ExtractedContent = {
existingDocs: [
{
path: "docs/guide.md",
title: "Guide",
content: "This is a general guide with no code references",
category: "tutorial",
},
],
adrs: [],
codeExamples: [],
apiDocs: [],
};
const docSections = await createDocumentationEntities(
projectId,
extractedContent,
);
const edges = await linkCodeToDocs(codeFiles, docSections);
// Should not create edges between unrelated code and docs
expect(edges.length).toBe(0);
});
});
});
```
--------------------------------------------------------------------------------
/tests/tools/recommend-ssg-historical.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for Phase 2.1: Historical Deployment Data Integration
* Tests the enhanced recommend_ssg tool with knowledge graph integration
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
initializeKnowledgeGraph,
createOrUpdateProject,
trackDeployment,
getMemoryManager,
} from "../../src/memory/kg-integration.js";
import { recommendSSG } from "../../src/tools/recommend-ssg.js";
import { MemoryManager } from "../../src/memory/manager.js";
describe("recommendSSG with Historical Data (Phase 2.1)", () => {
let testDir: string;
let originalEnv: string | undefined;
let memoryManager: MemoryManager;
beforeEach(async () => {
// Create temporary test directory
testDir = join(tmpdir(), `recommend-ssg-historical-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
// Set environment variable for storage
originalEnv = process.env.DOCUMCP_STORAGE_DIR;
process.env.DOCUMCP_STORAGE_DIR = testDir;
// Initialize KG and memory - this creates the global memory manager
await initializeKnowledgeGraph(testDir);
// Use the same memory manager instance that kg-integration created
memoryManager = await getMemoryManager();
});
afterEach(async () => {
// Restore environment
if (originalEnv) {
process.env.DOCUMCP_STORAGE_DIR = originalEnv;
} else {
delete process.env.DOCUMCP_STORAGE_DIR;
}
// Clean up test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch (error) {
console.warn("Failed to clean up test directory:", error);
}
});
describe("Historical Data Retrieval", () => {
it("should include historical data when similar projects exist", async () => {
// Create a project with successful deployments
const project1 = await createOrUpdateProject({
id: "test_project_1",
timestamp: new Date().toISOString(),
path: "/test/project1",
projectName: "Test Project 1",
structure: {
totalFiles: 50,
languages: { typescript: 30, javascript: 20 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
// Track successful Docusaurus deployments
await trackDeployment(project1.id, "docusaurus", true, {
buildTime: 45,
});
await trackDeployment(project1.id, "docusaurus", true, {
buildTime: 42,
});
// Store analysis in memory for recommendation
const memoryEntry = await memoryManager.remember("analysis", {
path: "/test/project2",
dependencies: {
ecosystem: "javascript",
languages: ["typescript", "javascript"],
},
structure: { totalFiles: 60 },
});
// Get recommendation
const result = await recommendSSG({
analysisId: memoryEntry.id,
preferences: {},
});
const content = result.content[0];
expect(content.type).toBe("text");
const data = JSON.parse(content.text);
// Should include historical data
expect(data.historicalData).toBeDefined();
expect(data.historicalData.similarProjectCount).toBeGreaterThan(0);
expect(data.historicalData.successRates.docusaurus).toBeDefined();
expect(data.historicalData.successRates.docusaurus.rate).toBe(1.0);
expect(data.historicalData.successRates.docusaurus.sampleSize).toBe(2);
});
it("should boost confidence when historical success rate is high", async () => {
// Create multiple successful projects
for (let i = 0; i < 3; i++) {
const project = await createOrUpdateProject({
id: `project_${i}`,
timestamp: new Date().toISOString(),
path: `/test/project${i}`,
projectName: `Project ${i}`,
structure: {
totalFiles: 50,
languages: { typescript: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
// Track successful Hugo deployments
await trackDeployment(project.id, "hugo", true, { buildTime: 30 });
}
// Store analysis
const memoryEntry = await memoryManager.remember("analysis", {
path: "/test/new-project",
dependencies: {
ecosystem: "go",
languages: ["typescript"],
},
structure: { totalFiles: 60 },
});
const result = await recommendSSG({ analysisId: memoryEntry.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should have high confidence due to historical success
expect(data.confidence).toBeGreaterThan(0.9);
expect(data.reasoning[0]).toContain("100% success rate");
});
it("should reduce confidence when historical success rate is low", async () => {
// Create project with failed deployments
const project = await createOrUpdateProject({
id: "failing_project",
timestamp: new Date().toISOString(),
path: "/test/failing",
projectName: "Failing Project",
structure: {
totalFiles: 50,
languages: { python: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
// Track mostly failed Jekyll deployments
await trackDeployment(project.id, "jekyll", false, {
errorMessage: "Build failed",
});
await trackDeployment(project.id, "jekyll", false, {
errorMessage: "Build failed",
});
await trackDeployment(project.id, "jekyll", true, { buildTime: 60 });
// Store analysis
const memoryEntry003 = await memoryManager.remember("analysis", {
path: "/test/new-python",
dependencies: {
ecosystem: "python",
languages: ["python"],
},
structure: { totalFiles: 60 },
});
const result = await recommendSSG({ analysisId: memoryEntry003.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should have reduced confidence
expect(data.confidence).toBeLessThan(0.8);
expect(data.reasoning[0]).toContain("33% success rate");
});
it("should switch to top performer when significantly better", async () => {
// Create projects with mixed results
const project1 = await createOrUpdateProject({
id: "project_mixed_1",
timestamp: new Date().toISOString(),
path: "/test/mixed1",
projectName: "Mixed Project 1",
structure: {
totalFiles: 50,
languages: { javascript: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
// Docusaurus: 50% success rate (2 samples)
await trackDeployment(project1.id, "docusaurus", true);
await trackDeployment(project1.id, "docusaurus", false);
// Eleventy: 100% success rate (3 samples)
const project2 = await createOrUpdateProject({
id: "project_mixed_2",
timestamp: new Date().toISOString(),
path: "/test/mixed2",
projectName: "Mixed Project 2",
structure: {
totalFiles: 50,
languages: { javascript: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
await trackDeployment(project2.id, "eleventy", true);
await trackDeployment(project2.id, "eleventy", true);
await trackDeployment(project2.id, "eleventy", true);
// Store analysis preferring JavaScript
const memoryEntry004 = await memoryManager.remember("analysis", {
path: "/test/new-js",
dependencies: {
ecosystem: "javascript",
languages: ["javascript"],
},
structure: { totalFiles: 40 },
});
const result = await recommendSSG({ analysisId: memoryEntry004.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should switch to Eleventy due to better success rate
expect(data.recommended).toBe("eleventy");
expect(data.reasoning[0]).toContain("Switching to eleventy");
expect(data.reasoning[0]).toContain("100% success rate");
});
it("should mention top performer as alternative if not switching", async () => {
// Create successful Hugo deployments
const project = await createOrUpdateProject({
id: "hugo_success",
timestamp: new Date().toISOString(),
path: "/test/hugo",
projectName: "Hugo Success",
structure: {
totalFiles: 100,
languages: { go: 80, markdown: 20 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
await trackDeployment(project.id, "hugo", true);
await trackDeployment(project.id, "hugo", true);
// Store analysis for different ecosystem
const memoryEntry005 = await memoryManager.remember("analysis", {
path: "/test/new-python",
dependencies: {
ecosystem: "python",
languages: ["python"],
},
structure: { totalFiles: 60 },
});
const result = await recommendSSG({ analysisId: memoryEntry005.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should keep Python recommendation but mention Hugo
expect(data.recommended).toBe("mkdocs");
const hugoMention = data.reasoning.find((r: string) =>
r.includes("hugo"),
);
expect(hugoMention).toBeDefined();
});
it("should include deployment statistics in reasoning", async () => {
// Create multiple projects with various deployments
for (let i = 0; i < 3; i++) {
const project = await createOrUpdateProject({
id: `stats_project_${i}`,
timestamp: new Date().toISOString(),
path: `/test/stats${i}`,
projectName: `Stats Project ${i}`,
structure: {
totalFiles: 50,
languages: { typescript: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
await trackDeployment(project.id, "docusaurus", true);
await trackDeployment(project.id, "docusaurus", true);
}
const memoryEntry006 = await memoryManager.remember("analysis", {
path: "/test/stats-new",
dependencies: {
ecosystem: "javascript",
languages: ["typescript"],
},
structure: { totalFiles: 50 },
});
const result = await recommendSSG({ analysisId: memoryEntry006.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should mention deployment statistics
const statsReasoning = data.reasoning.find((r: string) =>
r.includes("deployment(s) across"),
);
expect(statsReasoning).toBeDefined();
expect(statsReasoning).toContain("6 deployment(s)");
expect(statsReasoning).toContain("3 similar project(s)");
});
});
describe("Historical Data Structure", () => {
it("should provide complete historical data structure", async () => {
const project = await createOrUpdateProject({
id: "structure_test",
timestamp: new Date().toISOString(),
path: "/test/structure",
projectName: "Structure Test",
structure: {
totalFiles: 50,
languages: { javascript: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
await trackDeployment(project.id, "jekyll", true);
await trackDeployment(project.id, "hugo", true);
await trackDeployment(project.id, "hugo", true);
const memoryEntry007 = await memoryManager.remember("analysis", {
path: "/test/structure-new",
dependencies: {
ecosystem: "javascript",
languages: ["javascript"],
},
structure: { totalFiles: 50 },
});
const result = await recommendSSG({ analysisId: memoryEntry007.id });
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.historicalData).toBeDefined();
expect(data.historicalData.similarProjectCount).toBe(1);
expect(data.historicalData.successRates).toBeDefined();
expect(data.historicalData.successRates.jekyll).toEqual({
rate: 1.0,
sampleSize: 1,
});
expect(data.historicalData.successRates.hugo).toEqual({
rate: 1.0,
sampleSize: 2,
});
expect(data.historicalData.topPerformer).toBeDefined();
expect(data.historicalData.topPerformer?.ssg).toBe("hugo");
expect(data.historicalData.topPerformer?.deploymentCount).toBe(2);
});
it("should handle no historical data gracefully", async () => {
const memoryEntry008 = await memoryManager.remember("analysis", {
path: "/test/no-history",
dependencies: {
ecosystem: "ruby",
languages: ["ruby"],
},
structure: { totalFiles: 30 },
});
const result = await recommendSSG({ analysisId: memoryEntry008.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should still make recommendation
expect(data.recommended).toBe("jekyll");
expect(data.confidence).toBeGreaterThan(0);
// Historical data should show no similar projects
expect(data.historicalData).toBeDefined();
expect(data.historicalData.similarProjectCount).toBe(0);
expect(Object.keys(data.historicalData.successRates)).toHaveLength(0);
});
});
describe("Edge Cases", () => {
it("should handle single deployment samples cautiously", async () => {
const project = await createOrUpdateProject({
id: "single_sample",
timestamp: new Date().toISOString(),
path: "/test/single",
projectName: "Single Sample",
structure: {
totalFiles: 50,
languages: { python: 50 },
hasTests: true,
hasCI: false,
hasDocs: false,
},
});
// Single successful deployment
await trackDeployment(project.id, "mkdocs", true);
const memoryEntry009 = await memoryManager.remember("analysis", {
path: "/test/single-new",
dependencies: {
ecosystem: "python",
languages: ["python"],
},
structure: { totalFiles: 50 },
});
const result = await recommendSSG({ analysisId: memoryEntry009.id });
const content = result.content[0];
const data = JSON.parse(content.text);
// Should not be a top performer with only 1 sample
expect(data.historicalData?.topPerformer).toBeUndefined();
});
it("should handle knowledge graph initialization failure", async () => {
// Use invalid storage directory
const invalidDir = "/invalid/path/that/does/not/exist";
const memoryEntry010 = await memoryManager.remember("analysis", {
path: "/test/kg-fail",
dependencies: {
ecosystem: "javascript",
languages: ["javascript"],
},
structure: { totalFiles: 50 },
});
// Should still make recommendation despite KG failure
const result = await recommendSSG({ analysisId: memoryEntry010.id });
const content = result.content[0];
const data = JSON.parse(content.text);
expect(data.recommended).toBeDefined();
expect(data.confidence).toBeGreaterThan(0);
});
});
});
```
--------------------------------------------------------------------------------
/src/memory/deployment-analytics.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Deployment Analytics Module
* Phase 2.4: Pattern Analysis and Insights
*
* Analyzes deployment history to identify patterns, trends, and provide insights
*/
import { getKnowledgeGraph } from "./kg-integration.js";
import { GraphNode, GraphEdge } from "./knowledge-graph.js";
export interface DeploymentPattern {
ssg: string;
totalDeployments: number;
successfulDeployments: number;
failedDeployments: number;
successRate: number;
averageBuildTime?: number;
commonTechnologies: string[];
projectCount: number;
}
export interface DeploymentTrend {
period: string;
deployments: number;
successRate: number;
topSSG: string;
}
export interface DeploymentInsight {
type: "success" | "warning" | "recommendation";
title: string;
description: string;
ssg?: string;
metric?: number;
}
export interface AnalyticsReport {
summary: {
totalProjects: number;
totalDeployments: number;
overallSuccessRate: number;
mostUsedSSG: string;
mostSuccessfulSSG: string;
};
patterns: DeploymentPattern[];
insights: DeploymentInsight[];
recommendations: string[];
}
/**
* Deployment Analytics Engine
*/
export class DeploymentAnalytics {
/**
* Generate comprehensive analytics report
*/
async generateReport(): Promise<AnalyticsReport> {
const kg = await getKnowledgeGraph();
// Get all projects and deployments
const projects = await kg.findNodes({ type: "project" });
const deploymentEdges = await kg.findEdges({
properties: { baseType: "project_deployed_with" },
});
// Aggregate deployment data by SSG
const ssgStats = await this.aggregateSSGStatistics(
projects,
deploymentEdges,
);
// Calculate summary metrics
const summary = this.calculateSummary(ssgStats, projects.length);
// Identify patterns
const patterns = this.identifyPatterns(ssgStats);
// Generate insights
const insights = this.generateInsights(patterns, summary);
// Generate recommendations
const recommendations = this.generateRecommendations(patterns, insights);
return {
summary,
patterns,
insights,
recommendations,
};
}
/**
* Get deployment statistics for a specific SSG
*/
async getSSGStatistics(ssg: string): Promise<DeploymentPattern | null> {
const kg = await getKnowledgeGraph();
const deployments = await kg.findEdges({
properties: { baseType: "project_deployed_with" },
});
const allNodes = await kg.getAllNodes();
// Filter deployments for this SSG
const ssgDeployments = deployments.filter((edge) => {
const configNode = allNodes.find((n) => n.id === edge.target);
return configNode?.properties.ssg === ssg;
});
if (ssgDeployments.length === 0) {
return null;
}
const successful = ssgDeployments.filter(
(d) => d.properties.success,
).length;
const failed = ssgDeployments.length - successful;
// Calculate average build time
const buildTimes = ssgDeployments
.filter((d) => d.properties.buildTime)
.map((d) => d.properties.buildTime as number);
const averageBuildTime =
buildTimes.length > 0
? buildTimes.reduce((a, b) => a + b, 0) / buildTimes.length
: undefined;
// Get unique projects using this SSG
const projectIds = new Set(ssgDeployments.map((d) => d.source));
// Get common technologies from projects
const technologies = new Set<string>();
for (const projectId of projectIds) {
const project = allNodes.find((n) => n.id === projectId);
if (project?.properties.technologies) {
project.properties.technologies.forEach((tech: string) =>
technologies.add(tech),
);
}
}
return {
ssg,
totalDeployments: ssgDeployments.length,
successfulDeployments: successful,
failedDeployments: failed,
successRate: successful / ssgDeployments.length,
averageBuildTime,
commonTechnologies: Array.from(technologies),
projectCount: projectIds.size,
};
}
/**
* Compare multiple SSGs
*/
async compareSSGs(
ssgs: string[],
): Promise<{ ssg: string; pattern: DeploymentPattern }[]> {
const comparisons: { ssg: string; pattern: DeploymentPattern }[] = [];
for (const ssg of ssgs) {
const pattern = await this.getSSGStatistics(ssg);
if (pattern) {
comparisons.push({ ssg, pattern });
}
}
// Sort by success rate
return comparisons.sort(
(a, b) => b.pattern.successRate - a.pattern.successRate,
);
}
/**
* Identify deployment trends over time
*/
async identifyTrends(periodDays: number = 30): Promise<DeploymentTrend[]> {
const kg = await getKnowledgeGraph();
const deployments = await kg.findEdges({
properties: { baseType: "project_deployed_with" },
});
// Group deployments by time period
const now = Date.now();
const periodMs = periodDays * 24 * 60 * 60 * 1000;
const trends: Map<string, DeploymentTrend> = new Map();
for (const deployment of deployments) {
const timestamp = deployment.properties.timestamp;
if (!timestamp) continue;
const deploymentTime = new Date(timestamp).getTime();
const periodsAgo = Math.floor((now - deploymentTime) / periodMs);
if (periodsAgo < 0 || periodsAgo > 12) continue; // Last 12 periods
const periodKey = `${periodsAgo} periods ago`;
if (!trends.has(periodKey)) {
trends.set(periodKey, {
period: periodKey,
deployments: 0,
successRate: 0,
topSSG: "",
});
}
const trend = trends.get(periodKey)!;
trend.deployments++;
if (deployment.properties.success) {
trend.successRate++;
}
}
// Calculate success rates and identify top SSG per period
for (const trend of trends.values()) {
trend.successRate = trend.successRate / trend.deployments;
}
return Array.from(trends.values()).sort((a, b) =>
a.period.localeCompare(b.period),
);
}
/**
* Get deployment health score (0-100)
*/
async getHealthScore(): Promise<{
score: number;
factors: {
name: string;
impact: number;
status: "good" | "warning" | "critical";
}[];
}> {
const report = await this.generateReport();
const factors: {
name: string;
impact: number;
status: "good" | "warning" | "critical";
}[] = [];
let totalScore = 0;
// Factor 1: Overall success rate (40 points)
const successRateScore = report.summary.overallSuccessRate * 40;
totalScore += successRateScore;
factors.push({
name: "Overall Success Rate",
impact: successRateScore,
status:
report.summary.overallSuccessRate > 0.8
? "good"
: report.summary.overallSuccessRate > 0.5
? "warning"
: "critical",
});
// Factor 2: Number of projects (20 points)
const projectScore = Math.min(20, report.summary.totalProjects * 2);
totalScore += projectScore;
factors.push({
name: "Active Projects",
impact: projectScore,
status:
report.summary.totalProjects > 5
? "good"
: report.summary.totalProjects > 2
? "warning"
: "critical",
});
// Factor 3: Deployment frequency (20 points)
const deploymentScore = Math.min(20, report.summary.totalDeployments * 1.5);
totalScore += deploymentScore;
factors.push({
name: "Deployment Activity",
impact: deploymentScore,
status:
report.summary.totalDeployments > 10
? "good"
: report.summary.totalDeployments > 5
? "warning"
: "critical",
});
// Factor 4: SSG diversity (20 points)
const ssgDiversity = report.patterns.length;
const diversityScore = Math.min(20, ssgDiversity * 5);
totalScore += diversityScore;
factors.push({
name: "SSG Diversity",
impact: diversityScore,
status:
ssgDiversity > 3 ? "good" : ssgDiversity > 1 ? "warning" : "critical",
});
return {
score: Math.round(totalScore),
factors,
};
}
/**
* Private: Aggregate SSG statistics
*/
private async aggregateSSGStatistics(
projects: GraphNode[],
deploymentEdges: GraphEdge[],
): Promise<Map<string, DeploymentPattern>> {
const kg = await getKnowledgeGraph();
const allNodes = await kg.getAllNodes();
const ssgStats = new Map<string, DeploymentPattern>();
for (const deployment of deploymentEdges) {
const configNode = allNodes.find((n) => n.id === deployment.target);
if (!configNode || configNode.type !== "configuration") continue;
const ssg = configNode.properties.ssg;
if (!ssg) continue;
if (!ssgStats.has(ssg)) {
ssgStats.set(ssg, {
ssg,
totalDeployments: 0,
successfulDeployments: 0,
failedDeployments: 0,
successRate: 0,
commonTechnologies: [],
projectCount: 0,
});
}
const stats = ssgStats.get(ssg)!;
stats.totalDeployments++;
if (deployment.properties.success) {
stats.successfulDeployments++;
} else {
stats.failedDeployments++;
}
// Track build times
if (deployment.properties.buildTime) {
if (!stats.averageBuildTime) {
stats.averageBuildTime = 0;
}
stats.averageBuildTime += deployment.properties.buildTime;
}
}
// Calculate final metrics
for (const stats of ssgStats.values()) {
stats.successRate = stats.successfulDeployments / stats.totalDeployments;
if (stats.averageBuildTime) {
stats.averageBuildTime /= stats.totalDeployments;
}
}
return ssgStats;
}
/**
* Private: Calculate summary metrics
*/
private calculateSummary(
ssgStats: Map<string, DeploymentPattern>,
projectCount: number,
): AnalyticsReport["summary"] {
let totalDeployments = 0;
let totalSuccessful = 0;
let mostUsedSSG = "";
let mostUsedCount = 0;
let mostSuccessfulSSG = "";
let highestSuccessRate = 0;
for (const [ssg, stats] of ssgStats.entries()) {
totalDeployments += stats.totalDeployments;
totalSuccessful += stats.successfulDeployments;
if (stats.totalDeployments > mostUsedCount) {
mostUsedCount = stats.totalDeployments;
mostUsedSSG = ssg;
}
if (
stats.successRate > highestSuccessRate &&
stats.totalDeployments >= 2
) {
highestSuccessRate = stats.successRate;
mostSuccessfulSSG = ssg;
}
}
return {
totalProjects: projectCount,
totalDeployments,
overallSuccessRate:
totalDeployments > 0 ? totalSuccessful / totalDeployments : 0,
mostUsedSSG: mostUsedSSG || "none",
mostSuccessfulSSG: mostSuccessfulSSG || mostUsedSSG || "none",
};
}
/**
* Private: Identify patterns
*/
private identifyPatterns(
ssgStats: Map<string, DeploymentPattern>,
): DeploymentPattern[] {
return Array.from(ssgStats.values()).sort(
(a, b) => b.totalDeployments - a.totalDeployments,
);
}
/**
* Private: Generate insights
*/
private generateInsights(
patterns: DeploymentPattern[],
summary: AnalyticsReport["summary"],
): DeploymentInsight[] {
const insights: DeploymentInsight[] = [];
// Overall health insight
if (summary.overallSuccessRate > 0.8) {
insights.push({
type: "success",
title: "High Success Rate",
description: `Excellent! ${(summary.overallSuccessRate * 100).toFixed(
1,
)}% of deployments succeed`,
metric: summary.overallSuccessRate,
});
} else if (summary.overallSuccessRate < 0.5) {
insights.push({
type: "warning",
title: "Low Success Rate",
description: `Only ${(summary.overallSuccessRate * 100).toFixed(
1,
)}% of deployments succeed. Review common failure patterns.`,
metric: summary.overallSuccessRate,
});
}
// SSG-specific insights
for (const pattern of patterns) {
if (pattern.successRate === 1.0 && pattern.totalDeployments >= 3) {
insights.push({
type: "success",
title: `${pattern.ssg} Perfect Track Record`,
description: `All ${pattern.totalDeployments} deployments with ${pattern.ssg} succeeded`,
ssg: pattern.ssg,
metric: pattern.successRate,
});
} else if (pattern.successRate < 0.5 && pattern.totalDeployments >= 2) {
insights.push({
type: "warning",
title: `${pattern.ssg} Struggling`,
description: `Only ${(pattern.successRate * 100).toFixed(
0,
)}% success rate with ${pattern.ssg}`,
ssg: pattern.ssg,
metric: pattern.successRate,
});
}
// Build time insights
if (pattern.averageBuildTime) {
if (pattern.averageBuildTime < 30000) {
insights.push({
type: "success",
title: `${pattern.ssg} Fast Builds`,
description: `Average build time: ${(
pattern.averageBuildTime / 1000
).toFixed(1)}s`,
ssg: pattern.ssg,
metric: pattern.averageBuildTime,
});
} else if (pattern.averageBuildTime > 120000) {
insights.push({
type: "warning",
title: `${pattern.ssg} Slow Builds`,
description: `Average build time: ${(
pattern.averageBuildTime / 1000
).toFixed(1)}s. Consider optimization.`,
ssg: pattern.ssg,
metric: pattern.averageBuildTime,
});
}
}
}
return insights;
}
/**
* Private: Generate recommendations
*/
private generateRecommendations(
patterns: DeploymentPattern[],
insights: DeploymentInsight[],
): string[] {
const recommendations: string[] = [];
// Find best performing SSG
const bestSSG = patterns.find(
(p) => p.successRate > 0.8 && p.totalDeployments >= 2,
);
if (bestSSG) {
recommendations.push(
`Consider using ${bestSSG.ssg} for new projects (${(
bestSSG.successRate * 100
).toFixed(0)}% success rate)`,
);
}
// Identify problematic SSGs
const problematicSSG = patterns.find(
(p) => p.successRate < 0.5 && p.totalDeployments >= 3,
);
if (problematicSSG) {
recommendations.push(
`Review ${problematicSSG.ssg} deployment process - ${problematicSSG.failedDeployments} recent failures`,
);
}
// Diversity recommendation
if (patterns.length < 2) {
recommendations.push(
"Experiment with different SSGs to find the best fit for different project types",
);
}
// Activity recommendation
const totalDeployments = patterns.reduce(
(sum, p) => sum + p.totalDeployments,
0,
);
if (totalDeployments < 5) {
recommendations.push(
"Deploy more projects to build a robust historical dataset for better recommendations",
);
}
// Warning-based recommendations
const warnings = insights.filter((i) => i.type === "warning");
if (warnings.length > 2) {
recommendations.push(
"Multiple deployment issues detected - consider reviewing documentation setup process",
);
}
return recommendations;
}
}
/**
* Get singleton analytics instance
*/
let analyticsInstance: DeploymentAnalytics | null = null;
export function getDeploymentAnalytics(): DeploymentAnalytics {
if (!analyticsInstance) {
analyticsInstance = new DeploymentAnalytics();
}
return analyticsInstance;
}
```
--------------------------------------------------------------------------------
/docs/adrs/adr-0004-diataxis-framework-integration.md:
--------------------------------------------------------------------------------
```markdown
---
id: adr-4-diataxis-framework-integration
title: "ADR-004: Diataxis Framework Integration"
sidebar_label: "ADR-004: Diataxis Framework Integration"
sidebar_position: 4
documcp:
last_updated: "2025-01-14T00:00:00.000Z"
last_validated: "2025-01-14T00:00:00.000Z"
auto_updated: false
update_frequency: monthly
validated_against_commit: 9bbac23
---
# ADR-004: Diataxis Framework Integration for Documentation Structure
## Status
Accepted
## Context
DocuMCP aims to improve the quality and effectiveness of technical documentation by implementing proven information architecture principles. The Diataxis framework provides a systematic approach to organizing technical documentation into four distinct categories that serve different user needs and learning contexts.
Diataxis Framework Components:
- **Tutorials**: Learning-oriented content for skill acquisition (study context)
- **How-to Guides**: Problem-solving oriented content for specific tasks (work context)
- **Reference**: Information-oriented content for lookup and verification (information context)
- **Explanation**: Understanding-oriented content for context and background (understanding context)
Current documentation challenges:
- Most projects mix different content types without clear organization
- Users struggle to find appropriate content for their current needs
- Documentation often fails to serve different user contexts effectively
- Information architecture is typically ad-hoc and inconsistent
The framework addresses fundamental differences in user intent:
- **Study vs. Work**: Different contexts require different content approaches
- **Acquisition vs. Application**: Learning new skills vs. applying existing knowledge
- **Practical vs. Theoretical**: Task completion vs. understanding concepts
## Decision
We will integrate the Diataxis framework as the foundational information architecture for all DocuMCP-generated documentation structures, with intelligent content planning and navigation generation adapted to each static site generator's capabilities.
### Integration Approach:
#### 1. Automated Structure Generation
- **Directory organization** that clearly separates Diataxis content types
- **Navigation systems** that help users understand content categorization
- **Template generation** for each content type with appropriate guidance
- **Cross-reference systems** that maintain logical relationships between content types
#### 2. Content Type Templates
- **Tutorial templates** with learning objectives, prerequisites, step-by-step instructions
- **How-to guide templates** focused on problem-solution patterns
- **Reference templates** for systematic information organization
- **Explanation templates** for conceptual and architectural content
#### 3. Content Planning Intelligence
- **Automated content suggestions** based on project analysis
- **Gap identification** for missing content types
- **User journey mapping** to appropriate content categories
- **Content relationship mapping** to ensure comprehensive coverage
#### 4. SSG-Specific Implementation
- **Adaptation to SSG capabilities** while maintaining Diataxis principles
- **Theme and plugin recommendations** that support Diataxis organization
- **Navigation configuration** optimized for each SSG's features
## Alternatives Considered
### Generic Documentation Templates
- **Pros**: Simpler implementation, fewer constraints on content organization
- **Cons**: Perpetuates existing documentation quality problems, no systematic improvement
- **Decision**: Rejected due to missed opportunity for significant quality improvement
### Custom Documentation Framework
- **Pros**: Full control over documentation approach and features
- **Cons**: Reinventing proven methodology, reduced credibility, maintenance burden
- **Decision**: Rejected in favor of proven, established framework
### Multiple Framework Options
- **Pros**: Could accommodate different project preferences and approaches
- **Cons**: Choice paralysis, inconsistent quality, complex implementation
- **Decision**: Rejected to maintain focus and ensure consistent quality outcomes
### Optional Diataxis Integration
- **Pros**: Gives users choice, accommodates existing documentation structures
- **Cons**: Reduces value proposition, complicates implementation, inconsistent results
- **Decision**: Rejected to ensure consistent quality and educational value
## Consequences
### Positive
- **Improved Documentation Quality**: Systematic application of proven principles
- **Better User Experience**: Users can find appropriate content for their context
- **Educational Value**: Projects learn proper documentation organization
- **Consistency**: All DocuMCP projects benefit from same high-quality structure
- **Maintenance Benefits**: Clear content types simplify ongoing documentation work
### Negative
- **Learning Curve**: Teams need to understand Diataxis principles for optimal results
- **Initial Overhead**: More structure requires more initial planning and content creation
- **Rigidity**: Some projects might prefer different organizational approaches
### Risks and Mitigations
- **User Resistance**: Provide clear education about benefits and implementation guidance
- **Implementation Complexity**: Start with basic structure, enhance over time
- **Content Quality**: Provide high-quality templates and examples
## Implementation Details
### Diataxis Type Tracking in Code Examples (Phase 3 Enhancement)
Code examples in documentation are now tracked with Diataxis type information to enable context-aware validation and better content organization:
```typescript
interface CodeExample {
language: string;
code: string;
description: string;
referencedSymbols: string[];
diataxisType?: "tutorial" | "how-to" | "reference" | "explanation";
validationHints?: {
expectedBehavior?: string;
dependencies?: string[];
contextRequired?: boolean;
};
}
```
**Benefits**:
- Context-aware validation based on Diataxis category
- Improved code example organization and discovery
- Better drift detection for category-specific examples
- Enhanced content accuracy validation
**Implementation**: The drift detection system (ADR-009) uses Diataxis type information to provide category-specific validation rules and priority scoring.
### Directory Structure Generation
```typescript
interface DiataxisStructure {
tutorials: DirectoryConfig;
howToGuides: DirectoryConfig;
reference: DirectoryConfig;
explanation: DirectoryConfig;
navigation: NavigationConfig;
}
const DIATAXIS_TEMPLATES: Record<SSGType, DiataxisStructure> = {
hugo: {
tutorials: { path: "content/tutorials", layout: "tutorial" },
howToGuides: { path: "content/how-to", layout: "guide" },
reference: { path: "content/reference", layout: "reference" },
explanation: { path: "content/explanation", layout: "explanation" },
navigation: { menu: "diataxis", weight: "category-based" },
},
// ... other SSG configurations
};
```
### Content Template System
```typescript
interface ContentTemplate {
frontmatter: Record<string, any>;
structure: ContentSection[];
guidance: string[];
examples: string[];
}
const TUTORIAL_TEMPLATE: ContentTemplate = {
frontmatter: {
title: "{{ tutorial_title }}",
description: "{{ tutorial_description }}",
difficulty: "{{ difficulty_level }}",
prerequisites: "{{ prerequisites }}",
estimated_time: "{{ time_estimate }}",
},
structure: [
{ section: "learning_objectives", required: true },
{ section: "prerequisites", required: true },
{ section: "step_by_step_instructions", required: true },
{ section: "verification", required: true },
{ section: "next_steps", required: false },
],
guidance: [
"Focus on learning and skill acquisition",
"Provide complete, working examples",
"Include verification steps for each major milestone",
"Assume minimal prior knowledge",
],
};
```
### Content Planning Algorithm
```typescript
interface ContentPlan {
tutorials: TutorialSuggestion[];
howToGuides: HowToSuggestion[];
reference: ReferenceSuggestion[];
explanation: ExplanationSuggestion[];
}
function generateContentPlan(projectAnalysis: ProjectAnalysis): ContentPlan {
return {
tutorials: suggestTutorials(projectAnalysis),
howToGuides: suggestHowToGuides(projectAnalysis),
reference: suggestReference(projectAnalysis),
explanation: suggestExplanation(projectAnalysis),
};
}
function suggestTutorials(analysis: ProjectAnalysis): TutorialSuggestion[] {
const suggestions: TutorialSuggestion[] = [];
// Getting started tutorial (always recommended)
suggestions.push({
title: "Getting Started",
description: "First steps with {{ project_name }}",
priority: "high",
estimated_effort: "medium",
});
// Feature-specific tutorials based on project complexity
if (analysis.complexity.apiSurface > 5) {
suggestions.push({
title: "API Integration Tutorial",
description: "Complete guide to integrating with the API",
priority: "high",
estimated_effort: "large",
});
}
return suggestions;
}
```
### Navigation Generation
```typescript
interface DiataxisNavigation {
structure: NavigationItem[];
labels: NavigationLabels;
descriptions: CategoryDescriptions;
}
const NAVIGATION_STRUCTURE: DiataxisNavigation = {
structure: [
{
category: "tutorials",
label: "Tutorials",
description: "Learning-oriented guides",
icon: "graduation-cap",
order: 1,
},
{
category: "how-to",
label: "How-to Guides",
description: "Problem-solving recipes",
icon: "tools",
order: 2,
},
{
category: "reference",
label: "Reference",
description: "Technical information",
icon: "book",
order: 3,
},
{
category: "explanation",
label: "Explanation",
description: "Understanding and context",
icon: "lightbulb",
order: 4,
},
],
labels: {
tutorials: "Learn",
howToGuides: "Solve",
reference: "Lookup",
explanation: "Understand",
},
descriptions: {
tutorials: "Step-by-step learning paths",
howToGuides: "Solutions to specific problems",
reference: "Complete technical details",
explanation: "Background and concepts",
},
};
```
### SSG-Specific Adaptations
```typescript
interface SSGDiataxisAdapter {
generateStructure(
ssg: SSGType,
project: ProjectAnalysis,
): DiataxisImplementation;
createNavigation(
ssg: SSGType,
structure: DiataxisStructure,
): NavigationConfig;
generateTemplates(ssg: SSGType, contentTypes: ContentType[]): TemplateSet;
}
class HugoDiataxisAdapter implements SSGDiataxisAdapter {
generateStructure(
ssg: SSGType,
project: ProjectAnalysis,
): DiataxisImplementation {
return {
contentDirectories: this.createHugoContentStructure(),
frontmatterSchemas: this.createHugoFrontmatter(),
taxonomies: this.createDiataxisTaxonomies(),
menuConfiguration: this.createHugoMenus(),
};
}
createHugoContentStructure(): ContentStructure {
return {
"content/tutorials/": { weight: 10, section: "tutorials" },
"content/how-to/": { weight: 20, section: "guides" },
"content/reference/": { weight: 30, section: "reference" },
"content/explanation/": { weight: 40, section: "explanation" },
};
}
}
```
## Quality Assurance
### Diataxis Compliance Validation
```typescript
interface DiataxisValidator {
validateStructure(documentation: DocumentationStructure): ValidationResult;
checkContentTypeAlignment(
content: Content,
declaredType: ContentType,
): AlignmentResult;
identifyMissingCategories(structure: DocumentationStructure): Gap[];
}
function validateDiataxisCompliance(
docs: DocumentationStructure,
): ComplianceReport {
return {
structureCompliance: checkDirectoryOrganization(docs),
contentTypeAccuracy: validateContentCategorization(docs),
navigationClarity: assessNavigationEffectiveness(docs),
crossReferenceCompleteness: checkContentRelationships(docs),
};
}
```
### Content Quality Guidelines
- **Tutorial Content**: Must include learning objectives, prerequisites, and verification steps
- **How-to Content**: Must focus on specific problems with clear solution steps
- **Reference Content**: Must be comprehensive, accurate, and systematically organized
- **Explanation Content**: Must provide context, background, and conceptual understanding
### Testing Strategy
- **Structure Tests**: Validate directory organization and navigation generation
- **Template Tests**: Ensure all content type templates are properly formatted
- **Integration Tests**: Test complete Diataxis implementation across different SSGs
- **User Experience Tests**: Validate that users can effectively navigate and find content
## Educational Integration
### User Guidance
- **Diataxis Explanation**: Clear documentation of framework benefits and principles
- **Content Type Guidelines**: Detailed guidance for creating each type of content
- **Migration Assistance**: Help converting existing documentation to Diataxis structure
- **Best Practice Examples**: Templates and examples demonstrating effective implementation
### Community Building
- **Diataxis Advocacy**: Promote framework adoption across open-source community
- **Success Story Sharing**: Highlight projects benefiting from Diataxis implementation
- **Training Resources**: Develop educational materials for technical writers and maintainers
- **Feedback Collection**: Gather community input for framework implementation improvements
## Future Enhancements
### Advanced Features
- **Content Gap Analysis**: AI-powered identification of missing content areas
- **User Journey Optimization**: Intelligent linking between content types based on user flows
- **Content Quality Scoring**: Automated assessment of content quality within each category
- **Personalized Navigation**: Adaptive navigation based on user role and experience level
### Tool Integration
- **Analytics Integration**: Track how users navigate between different content types
- **Content Management**: Tools for maintaining Diataxis compliance over time
- **Translation Support**: Multi-language implementations of Diataxis structure
- **Accessibility Features**: Ensure Diataxis implementation supports accessibility standards
## Implementation Status
**Status**: ✅ Implemented (2025-12-12)
**Implementation Files**:
- `src/tools/populate-content.ts` - Diataxis content population engine
- `src/prompts/technical-writer-prompts.ts` - Diataxis-aware prompt generation
- `src/utils/drift-detector.ts` - Diataxis type tracking in code examples
- `docs/tutorials/`, `docs/how-to/`, `docs/reference/`, `docs/explanation/` - Diataxis structure in use
**Key Features Implemented**:
- ✅ Automated Diataxis structure generation
- ✅ Content type templates (tutorials, how-to guides, reference, explanation)
- ✅ Content planning intelligence based on project analysis
- ✅ SSG-specific Diataxis adaptations
- ✅ Diataxis type tracking in code examples (Phase 3 enhancement)
- ✅ Navigation generation aligned with Diataxis principles
**Validation**: The framework is actively used in the documentation structure and content generation tools. Code examples include Diataxis type information for context-aware validation.
## References
- [Diataxis Framework Official Documentation](https://diataxis.fr/)
- [Information Architecture Principles](https://www.usability.gov/what-and-why/information-architecture.html)
- [Technical Writing Best Practices](https://developers.google.com/tech-writing)
- Commit: 9bbac23 - feat: Add Diataxis type tracking to CodeExample interface (#81)
- GitHub Issue: #81 - Diataxis type tracking to CodeExample interface
```
--------------------------------------------------------------------------------
/tests/utils/semantic-analyzer.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for Semantic Analyzer
*/
import {
SemanticAnalyzer,
createSemanticAnalyzer,
type SemanticAnalysisOptions,
type EnhancedSemanticAnalysis,
} from '../../src/utils/semantic-analyzer.js';
import { createLLMClient } from '../../src/utils/llm-client.js';
// Mock the LLM client module
jest.mock('../../src/utils/llm-client.js', () => {
const actual = jest.requireActual('../../src/utils/llm-client.js');
return {
...actual,
createLLMClient: jest.fn(),
};
});
const mockCreateLLMClient = createLLMClient as jest.MockedFunction<typeof createLLMClient>;
describe('SemanticAnalyzer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('createSemanticAnalyzer', () => {
test('should create analyzer with default options', () => {
const analyzer = createSemanticAnalyzer();
expect(analyzer).toBeInstanceOf(SemanticAnalyzer);
});
test('should create analyzer with custom options', () => {
const analyzer = createSemanticAnalyzer({
confidenceThreshold: 0.8,
useLLM: false,
});
expect(analyzer).toBeInstanceOf(SemanticAnalyzer);
});
});
describe('SemanticAnalyzer without LLM', () => {
let analyzer: SemanticAnalyzer;
beforeEach(async () => {
mockCreateLLMClient.mockReturnValue(null);
analyzer = new SemanticAnalyzer({ useLLM: false });
await analyzer.initialize();
});
test('should initialize successfully', async () => {
expect(analyzer).toBeDefined();
});
test('should report LLM as unavailable', () => {
expect(analyzer.isLLMAvailable()).toBe(false);
});
describe('analyzeSemanticImpact - AST mode', () => {
test('should detect no changes when code is identical', async () => {
const code = 'function test(x: number) { return x * 2; }';
const result = await analyzer.analyzeSemanticImpact(code, code);
expect(result.analysisMode).toBe('ast');
expect(result.hasBehavioralChange).toBe(false);
expect(result.llmAvailable).toBe(false);
expect(result.timestamp).toBeDefined();
});
test('should detect parameter changes', async () => {
const before = 'function test(x: number) { return x * 2; }';
const after = 'function test(x: number, y: string) { return x * 2; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.hasBehavioralChange).toBe(true);
expect(result.breakingForExamples).toBe(true);
expect(result.changeDescription).toContain('Breaking changes');
expect(result.astDiffs).toBeDefined();
expect(result.astDiffs!.length).toBeGreaterThan(0);
});
test('should detect async modifier changes', async () => {
const before = 'function test() { return 42; }';
const after = 'async function test() { return 42; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.hasBehavioralChange).toBe(true);
expect(result.astDiffs).toBeDefined();
expect(result.astDiffs!.some(d => d.details.includes('Async'))).toBe(true);
});
test('should detect return type changes', async () => {
const before = 'function test(): number { return 42; }';
const after = 'function test(): string { return "42"; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.hasBehavioralChange).toBe(true);
expect(result.breakingForExamples).toBe(true);
expect(result.astDiffs).toBeDefined();
});
test('should detect implementation changes', async () => {
const before = 'function test(x: number) { return x * 2; }';
const after = 'function test(x: number) { return x * 3; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.hasBehavioralChange).toBe(true);
expect(result.astDiffs).toBeDefined();
});
test('should identify affected documentation sections', async () => {
const before = 'function test(x: number): number { return x; }';
const after = 'function test(x: string): number { return 0; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.affectedDocSections).toContain('API Reference');
});
test('should have moderate confidence for AST analysis', async () => {
const before = 'function test(x: number) { return x * 2; }';
const after = 'function test(x: number) { return x * 3; }';
const result = await analyzer.analyzeSemanticImpact(before, after);
expect(result.confidence).toBeGreaterThan(0);
expect(result.confidence).toBeLessThanOrEqual(1);
});
});
describe('validateExamples - without LLM', () => {
test('should require manual review when LLM unavailable', async () => {
const examples = ['const x = test(5);'];
const implementation = 'function test(n) { return n * 2; }';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.requiresManualReview).toBe(true);
expect(result.overallConfidence).toBe(0);
expect(result.suggestions).toContain('LLM not available - manual validation required');
});
});
describe('analyzeBatch', () => {
test('should analyze multiple changes', async () => {
const changes = [
{
before: 'function a(x: number) { return x; }',
after: 'function a(x: string) { return x; }',
name: 'a',
},
{
before: 'function b() { return 1; }',
after: 'async function b() { return 1; }',
name: 'b',
},
];
const results = await analyzer.analyzeBatch(changes);
expect(results).toHaveLength(2);
expect(results[0].analysisMode).toBe('ast');
expect(results[1].analysisMode).toBe('ast');
});
});
});
describe('SemanticAnalyzer with LLM', () => {
let analyzer: SemanticAnalyzer;
let mockLLMClient: any;
beforeEach(async () => {
mockLLMClient = {
complete: jest.fn(),
analyzeCodeChange: jest.fn(),
simulateExecution: jest.fn(),
};
mockCreateLLMClient.mockReturnValue(mockLLMClient);
analyzer = new SemanticAnalyzer({ useLLM: true });
await analyzer.initialize();
});
test('should report LLM as available', () => {
expect(analyzer.isLLMAvailable()).toBe(true);
});
describe('analyzeSemanticImpact - LLM mode', () => {
test('should use LLM analysis with high confidence', async () => {
const mockAnalysis = {
hasBehavioralChange: true,
breakingForExamples: false,
changeDescription: 'Implementation optimized',
affectedDocSections: ['Performance'],
confidence: 0.9,
};
mockLLMClient.analyzeCodeChange.mockResolvedValue(mockAnalysis);
const before = 'function test(x) { return x * 2; }';
const after = 'function test(x) { return x << 1; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.analysisMode).toBe('llm');
expect(result.hasBehavioralChange).toBe(true);
expect(result.confidence).toBe(0.9);
expect(result.llmAvailable).toBe(true);
expect(mockLLMClient.analyzeCodeChange).toHaveBeenCalledWith(before, after);
});
test('should use hybrid mode with low LLM confidence', async () => {
const mockAnalysis = {
hasBehavioralChange: true,
breakingForExamples: false,
changeDescription: 'Unclear change',
affectedDocSections: [],
confidence: 0.3, // Below threshold
};
mockLLMClient.analyzeCodeChange.mockResolvedValue(mockAnalysis);
const before = 'function test(x: number) { return x * 2; }';
const after = 'function test(x: number) { return x * 3; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.analysisMode).toBe('hybrid');
expect(result.astDiffs).toBeDefined();
expect(result.llmAvailable).toBe(true);
});
test('should fallback to AST when LLM fails', async () => {
mockLLMClient.analyzeCodeChange.mockRejectedValue(new Error('LLM error'));
const before = 'function test(x: number) { return x; }';
const after = 'function test(x: string) { return x; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.analysisMode).toBe('ast');
expect(result.llmAvailable).toBe(false);
});
test('should combine LLM and AST results in hybrid mode', async () => {
const mockAnalysis = {
hasBehavioralChange: false,
breakingForExamples: false,
changeDescription: 'Minor refactoring',
affectedDocSections: ['Code Style'],
confidence: 0.5,
};
mockLLMClient.analyzeCodeChange.mockResolvedValue(mockAnalysis);
const before = 'function test(x: number): number { return x; }';
const after = 'function test(x: string): string { return x; }';
const result = await analyzer.analyzeSemanticImpact(before, after, 'test');
expect(result.analysisMode).toBe('hybrid');
expect(result.hasBehavioralChange).toBe(true); // AST detected breaking change
expect(result.affectedDocSections.length).toBeGreaterThan(0);
expect(result.changeDescription).toContain('Minor refactoring');
expect(result.changeDescription).toContain('AST analysis');
});
});
describe('validateExamples - with LLM', () => {
test('should validate examples successfully', async () => {
const mockSimulation = {
success: true,
expectedOutput: '10',
actualOutput: '10',
matches: true,
differences: [],
confidence: 0.95,
};
mockLLMClient.simulateExecution.mockResolvedValue(mockSimulation);
const examples = ['const result = multiply(2, 5);'];
const implementation = 'function multiply(a, b) { return a * b; }';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.isValid).toBe(true);
expect(result.examples).toHaveLength(1);
expect(result.examples[0].isValid).toBe(true);
expect(result.overallConfidence).toBeGreaterThan(0.9);
expect(result.requiresManualReview).toBe(false);
});
test('should detect invalid examples', async () => {
const mockSimulation = {
success: true,
expectedOutput: '10',
actualOutput: '5',
matches: false,
differences: ['Output mismatch'],
confidence: 0.85,
};
mockLLMClient.simulateExecution.mockResolvedValue(mockSimulation);
const examples = ['const result = multiply(2, 5);'];
const implementation = 'function multiply(a, b) { return a + b; }';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.isValid).toBe(false);
expect(result.examples[0].isValid).toBe(false);
expect(result.examples[0].issues.length).toBeGreaterThan(0);
expect(result.suggestions).toContain('1 example(s) may be invalid');
});
test('should handle validation errors gracefully', async () => {
mockLLMClient.simulateExecution.mockRejectedValue(new Error('Simulation failed'));
const examples = ['const result = test();'];
const implementation = 'function test() {}';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.isValid).toBe(false);
expect(result.examples[0].isValid).toBe(false);
expect(result.examples[0].issues).toContain('Validation failed');
});
test('should recommend manual review for low confidence', async () => {
const mockSimulation = {
success: true,
expectedOutput: 'unknown',
actualOutput: 'unknown',
matches: true,
differences: [],
confidence: 0.4, // Below threshold
};
mockLLMClient.simulateExecution.mockResolvedValue(mockSimulation);
const examples = ['test();'];
const implementation = 'function test() {}';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.requiresManualReview).toBe(true);
expect(result.suggestions).toContain('Low confidence - manual review recommended');
});
test('should validate multiple examples', async () => {
mockLLMClient.simulateExecution
.mockResolvedValueOnce({
success: true,
expectedOutput: '10',
actualOutput: '10',
matches: true,
differences: [],
confidence: 0.9,
})
.mockResolvedValueOnce({
success: true,
expectedOutput: '20',
actualOutput: '20',
matches: true,
differences: [],
confidence: 0.85,
});
const examples = [
'const a = multiply(2, 5);',
'const b = multiply(4, 5);',
];
const implementation = 'function multiply(a, b) { return a * b; }';
const result = await analyzer.validateExamples(examples, implementation);
expect(result.isValid).toBe(true);
expect(result.examples).toHaveLength(2);
expect(result.overallConfidence).toBeCloseTo(0.875, 2);
});
});
describe('analyzeBatch - with LLM', () => {
test('should analyze multiple changes with LLM', async () => {
mockLLMClient.analyzeCodeChange
.mockResolvedValueOnce({
hasBehavioralChange: true,
breakingForExamples: false,
changeDescription: 'First change',
affectedDocSections: ['API'],
confidence: 0.9,
})
.mockResolvedValueOnce({
hasBehavioralChange: false,
breakingForExamples: false,
changeDescription: 'Second change',
affectedDocSections: [],
confidence: 0.8,
});
const changes = [
{ before: 'code1', after: 'code2', name: 'func1' },
{ before: 'code3', after: 'code4', name: 'func2' },
];
const results = await analyzer.analyzeBatch(changes);
expect(results).toHaveLength(2);
expect(results[0].analysisMode).toBe('llm');
expect(results[1].analysisMode).toBe('llm');
expect(mockLLMClient.analyzeCodeChange).toHaveBeenCalledTimes(2);
});
});
});
describe('Custom confidence threshold', () => {
test('should use custom threshold for hybrid mode decision', async () => {
const mockLLMClient = {
complete: jest.fn(),
analyzeCodeChange: jest.fn().mockResolvedValue({
hasBehavioralChange: true,
breakingForExamples: false,
changeDescription: 'Change',
affectedDocSections: [],
confidence: 0.75,
}),
simulateExecution: jest.fn(),
};
mockCreateLLMClient.mockReturnValue(mockLLMClient);
const analyzer = new SemanticAnalyzer({
useLLM: true,
confidenceThreshold: 0.8, // Higher than LLM confidence
});
await analyzer.initialize();
const result = await analyzer.analyzeSemanticImpact('code1', 'code2');
// Should use hybrid mode because confidence (0.75) < threshold (0.8)
expect(result.analysisMode).toBe('hybrid');
});
});
});
```
--------------------------------------------------------------------------------
/tests/tools/generate-readme-template.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import * as path from "path";
import * as tmp from "tmp";
import {
generateReadmeTemplate,
ReadmeTemplateGenerator,
GenerateReadmeTemplateSchema,
TemplateType,
} from "../../src/tools/generate-readme-template";
describe("README Template Generator", () => {
let tempDir: string;
let generator: ReadmeTemplateGenerator;
beforeEach(() => {
tempDir = tmp.dirSync({ unsafeCleanup: true }).name;
generator = new ReadmeTemplateGenerator();
});
afterEach(async () => {
try {
await fs.rmdir(tempDir, { recursive: true });
} catch {
// Ignore cleanup errors
}
});
describe("Input Validation", () => {
it("should validate required fields", () => {
expect(() => GenerateReadmeTemplateSchema.parse({})).toThrow();
expect(() =>
GenerateReadmeTemplateSchema.parse({
projectName: "",
description: "test",
}),
).toThrow();
expect(() =>
GenerateReadmeTemplateSchema.parse({
projectName: "test",
description: "",
}),
).toThrow();
});
it("should accept valid input with defaults", () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "test-project",
description: "A test project",
templateType: "library",
});
expect(input.license).toBe("MIT");
expect(input.includeScreenshots).toBe(false);
expect(input.includeBadges).toBe(true);
expect(input.includeContributing).toBe(true);
});
it("should validate template types", () => {
expect(() =>
GenerateReadmeTemplateSchema.parse({
projectName: "test",
description: "test",
templateType: "invalid-type",
}),
).toThrow();
const validTypes: TemplateType[] = [
"library",
"application",
"cli-tool",
"api",
"documentation",
];
for (const type of validTypes) {
expect(() =>
GenerateReadmeTemplateSchema.parse({
projectName: "test",
description: "test",
templateType: type,
}),
).not.toThrow();
}
});
});
describe("Template Generation", () => {
it("should generate library template correctly", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "awesome-lib",
description: "An awesome JavaScript library",
templateType: "library",
author: "john-doe",
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("# awesome-lib");
expect(result.content).toContain("> An awesome JavaScript library");
expect(result.content).toContain("npm install awesome-lib");
expect(result.content).toContain(
"const awesomeLib = require('awesome-lib')",
);
expect(result.content).toContain("## TL;DR");
expect(result.content).toContain("## Quick Start");
expect(result.content).toContain("## API Documentation");
expect(result.content).toContain("MIT © john-doe");
expect(result.metadata.templateType).toBe("library");
expect(result.metadata.estimatedLength).toBe(150);
});
it("should generate application template correctly", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "my-app",
description: "A web application",
templateType: "application",
author: "jane-doe",
includeScreenshots: true,
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("# my-app");
expect(result.content).toContain("> A web application");
expect(result.content).toContain("## What This Does");
expect(result.content).toContain(
"git clone https://github.com/jane-doe/my-app.git",
);
expect(result.content).toContain("npm start");
expect(result.content).toContain("## Configuration");
expect(result.content).toContain("![my-app Screenshot]");
expect(result.metadata.templateType).toBe("application");
});
it("should generate CLI tool template correctly", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "my-cli",
description: "A command line tool",
templateType: "cli-tool",
author: "dev-user",
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("# my-cli");
expect(result.content).toContain("npm install -g my-cli");
expect(result.content).toContain("npx my-cli --help");
expect(result.content).toContain("## Usage");
expect(result.content).toContain("## Options");
expect(result.content).toContain("| Option | Description | Default |");
expect(result.metadata.templateType).toBe("cli-tool");
});
it("should handle camelCase conversion correctly", () => {
const testCases = [
{ input: "my-awesome-lib", expected: "myAwesomeLib" },
{ input: "simple_package", expected: "simplePackage" },
{ input: "Mixed-Case_Name", expected: "mixedCaseName" },
{ input: "single", expected: "single" },
];
for (const testCase of testCases) {
const generator = new ReadmeTemplateGenerator();
const input = GenerateReadmeTemplateSchema.parse({
projectName: testCase.input,
description: "test",
templateType: "library",
});
const result = generator.generateTemplate(input);
expect(result).toContain(
`const ${testCase.expected} = require('${testCase.input}')`,
);
}
});
});
describe("Badge Generation", () => {
it("should include badges when enabled", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "badge-lib",
description: "Library with badges",
templateType: "library",
author: "dev",
includeBadges: true,
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("[![npm version]");
expect(result.content).toContain("[![Build Status]");
expect(result.content).toContain("[![License: MIT]");
expect(result.content).toContain("dev/badge-lib");
});
it("should exclude badges when disabled", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "no-badge-lib",
description: "Library without badges",
templateType: "library",
includeBadges: false,
});
const result = await generateReadmeTemplate(input);
expect(result.content).not.toContain("[",
);
expect(result.content).toContain("*Add a screenshot or demo GIF here*");
});
it("should exclude screenshots when disabled", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "no-screenshot-app",
description: "App without screenshots",
templateType: "application",
includeScreenshots: false,
});
const result = await generateReadmeTemplate(input);
expect(result.content).not.toContain("![visual-app Screenshot]");
});
});
describe("Contributing Section", () => {
it("should include contributing section when enabled", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "contrib-lib",
description: "Library with contributing section",
templateType: "library",
includeContributing: true,
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("## Contributing");
expect(result.content).toContain("CONTRIBUTING.md");
});
it("should exclude contributing section when disabled", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "no-contrib-lib",
description: "Library without contributing section",
templateType: "library",
includeContributing: false,
});
const result = await generateReadmeTemplate(input);
expect(result.content).not.toContain("## Contributing");
});
});
describe("File Output", () => {
it("should write to file when outputPath is specified", async () => {
const outputPath = path.join(tempDir, "README.md");
const input = GenerateReadmeTemplateSchema.parse({
projectName: "output-lib",
description: "Library with file output",
templateType: "library",
outputPath: outputPath,
});
const result = await generateReadmeTemplate(input);
await expect(fs.access(outputPath)).resolves.toBeUndefined();
const fileContent = await fs.readFile(outputPath, "utf-8");
expect(fileContent).toBe(result.content);
expect(fileContent).toContain("# output-lib");
});
it("should not write file when outputPath is not specified", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "no-file-test",
description: "Library without file output",
templateType: "library",
});
await generateReadmeTemplate(input);
const possiblePath = path.join(tempDir, "README.md");
await expect(fs.access(possiblePath)).rejects.toThrow();
});
});
describe("Template Metadata", () => {
it("should return correct metadata for each template type", () => {
const templateTypes: TemplateType[] = [
"library",
"application",
"cli-tool",
];
for (const type of templateTypes) {
const info = generator.getTemplateInfo(type);
expect(info).toBeDefined();
expect(info!.type).toBe(type);
expect(info!.estimatedLength).toBeGreaterThan(0);
}
});
it("should return null for invalid template type", () => {
const info = generator.getTemplateInfo("invalid" as TemplateType);
expect(info).toBeNull();
});
it("should count sections correctly", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "error-lib",
description: "Library that causes error",
templateType: "library",
});
const result = await generateReadmeTemplate(input);
const sectionCount = (result.content.match(/^##\s/gm) || []).length;
expect(result.metadata.sectionsIncluded).toBeGreaterThanOrEqual(
sectionCount,
);
expect(result.metadata.sectionsIncluded).toBeGreaterThan(3);
});
});
describe("Available Templates", () => {
it("should return list of available template types", () => {
const availableTypes = generator.getAvailableTemplates();
expect(availableTypes).toContain("library");
expect(availableTypes).toContain("application");
expect(availableTypes).toContain("cli-tool");
expect(availableTypes.length).toBeGreaterThan(0);
});
});
describe("Error Handling", () => {
it("should throw error for unsupported template type", async () => {
const generator = new ReadmeTemplateGenerator();
expect(() =>
generator.generateTemplate({
projectName: "test",
description: "test",
templateType: "unsupported" as TemplateType,
license: "MIT",
includeScreenshots: false,
includeBadges: true,
includeContributing: true,
}),
).toThrow('Template type "unsupported" not supported');
});
it("should handle file write errors gracefully", async () => {
const invalidPath = "/invalid/nonexistent/path/README.md";
const input = GenerateReadmeTemplateSchema.parse({
projectName: "error-test",
description: "test error handling",
templateType: "library",
outputPath: invalidPath,
});
await expect(generateReadmeTemplate(input)).rejects.toThrow();
});
});
describe("Variable Replacement", () => {
it("should replace all template variables correctly", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "license-lib",
description: "Library with custom license",
templateType: "library",
author: "dev",
license: "Apache-2.0",
});
const result = await generateReadmeTemplate(input);
expect(result.content).not.toContain("{{projectName}}");
expect(result.content).not.toContain("{{description}}");
expect(result.content).not.toContain("{{author}}");
expect(result.content).not.toContain("{{license}}");
expect(result.content).toContain("license-lib");
expect(result.content).toContain("Library with custom license");
expect(result.content).toContain("dev");
expect(result.content).toContain("Apache-2.0");
});
it("should use default values for missing optional fields", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "time-lib",
description: "Library with timing",
templateType: "library",
});
const result = await generateReadmeTemplate(input);
expect(result.content).toContain("your-username");
expect(result.content).toContain("MIT");
});
});
describe("Template Structure Validation", () => {
it("should generate valid markdown structure", async () => {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "structure-test",
description: "test structure",
templateType: "library",
});
const result = await generateReadmeTemplate(input);
// Check for proper heading hierarchy
const lines = result.content.split("\n");
const headings = lines.filter((line) => line.startsWith("#"));
expect(headings.length).toBeGreaterThan(0);
expect(headings[0]).toMatch(/^#\s+/); // Main title
// Check for code blocks
expect(result.content).toMatch(/```[\s\S]*?```/);
// Check for proper spacing
expect(result.content).not.toMatch(/#{1,6}\s*\n\s*#{1,6}/);
});
it("should maintain consistent formatting across templates", async () => {
const templateTypes: TemplateType[] = [
"library",
"application",
"cli-tool",
];
for (const type of templateTypes) {
const input = GenerateReadmeTemplateSchema.parse({
projectName: "format-test",
description: "test format",
templateType: type,
});
const result = await generateReadmeTemplate(input);
// All templates should have main title
expect(result.content).toMatch(/^#\s+format-test/m);
// All templates should have license section
expect(result.content).toContain("## License");
// All templates should end with license info
expect(result.content.trim()).toMatch(/MIT © your-username$/);
}
});
});
});
```
--------------------------------------------------------------------------------
/tests/memory/user-preferences.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for User Preference Management
*/
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
UserPreferenceManager,
getUserPreferenceManager,
clearPreferenceManagerCache,
} from "../../src/memory/user-preferences.js";
import {
getKnowledgeGraph,
initializeKnowledgeGraph,
} from "../../src/memory/kg-integration.js";
describe("UserPreferenceManager", () => {
let testDir: string;
beforeEach(async () => {
// Create temporary test directory
testDir = join(tmpdir(), `user-prefs-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
// Initialize KG with test directory
await initializeKnowledgeGraph(testDir);
clearPreferenceManagerCache();
});
afterEach(async () => {
clearPreferenceManagerCache();
// Clean up test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("Initialization", () => {
it("should create default preferences for new user", async () => {
const manager = new UserPreferenceManager("test-user");
await manager.initialize();
const prefs = await manager.getPreferences();
expect(prefs.userId).toBe("test-user");
expect(prefs.preferredSSGs).toEqual([]);
expect(prefs.documentationStyle).toBe("comprehensive");
expect(prefs.expertiseLevel).toBe("intermediate");
expect(prefs.autoApplyPreferences).toBe(true);
});
it("should load existing preferences from knowledge graph", async () => {
// Create a user with preferences
const kg = await getKnowledgeGraph();
kg.addNode({
id: "user:existing-user",
type: "user",
label: "existing-user",
properties: {
userId: "existing-user",
preferredSSGs: ["jekyll", "hugo"],
documentationStyle: "minimal",
expertiseLevel: "advanced",
preferredTechnologies: ["typescript"],
preferredDiataxisCategories: ["tutorials"],
autoApplyPreferences: false,
lastActive: "2025-01-01T00:00:00.000Z",
},
weight: 1.0,
});
const manager = new UserPreferenceManager("existing-user");
await manager.initialize();
const prefs = await manager.getPreferences();
expect(prefs.userId).toBe("existing-user");
expect(prefs.preferredSSGs).toEqual(["jekyll", "hugo"]);
expect(prefs.documentationStyle).toBe("minimal");
expect(prefs.expertiseLevel).toBe("advanced");
expect(prefs.autoApplyPreferences).toBe(false);
});
it("should handle getPreferences before initialization", async () => {
const manager = new UserPreferenceManager("auto-init");
const prefs = await manager.getPreferences();
expect(prefs.userId).toBe("auto-init");
expect(prefs.preferredSSGs).toEqual([]);
});
});
describe("Update Preferences", () => {
it("should update preferences and save to knowledge graph", async () => {
const manager = new UserPreferenceManager("update-test");
await manager.initialize();
await manager.updatePreferences({
documentationStyle: "tutorial-heavy",
expertiseLevel: "beginner",
preferredTechnologies: ["python", "go"],
});
const prefs = await manager.getPreferences();
expect(prefs.documentationStyle).toBe("tutorial-heavy");
expect(prefs.expertiseLevel).toBe("beginner");
expect(prefs.preferredTechnologies).toEqual(["python", "go"]);
});
it("should initialize before update if not already initialized", async () => {
const manager = new UserPreferenceManager("lazy-init");
await manager.updatePreferences({
expertiseLevel: "advanced",
});
const prefs = await manager.getPreferences();
expect(prefs.expertiseLevel).toBe("advanced");
});
});
describe("Track SSG Usage", () => {
it("should track successful SSG usage and create preference", async () => {
const manager = new UserPreferenceManager("ssg-tracker");
await manager.initialize();
await manager.trackSSGUsage({
ssg: "jekyll",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
const prefs = await manager.getPreferences();
expect(prefs.preferredSSGs).toContain("jekyll");
});
it("should track failed SSG usage", async () => {
const manager = new UserPreferenceManager("fail-tracker");
await manager.initialize();
await manager.trackSSGUsage({
ssg: "hugo",
success: false,
timestamp: "2025-01-01T00:00:00.000Z",
});
const kg = await getKnowledgeGraph();
const edges = await kg.findEdges({
type: "user_prefers_ssg",
});
expect(edges.length).toBeGreaterThan(0);
const edge = edges.find((e) => e.target.includes("hugo"));
expect(edge).toBeDefined();
expect(edge!.weight).toBe(0.5); // Failed usage has lower weight
});
it("should update existing SSG preference", async () => {
const manager = new UserPreferenceManager("update-tracker");
await manager.initialize();
// First usage - success
await manager.trackSSGUsage({
ssg: "docusaurus",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
// Second usage - success
await manager.trackSSGUsage({
ssg: "docusaurus",
success: true,
timestamp: "2025-01-02T00:00:00.000Z",
});
const kg = await getKnowledgeGraph();
const edges = await kg.findEdges({
type: "user_prefers_ssg",
});
const docEdge = edges.find((e) => e.target.includes("docusaurus"));
expect(docEdge!.properties.usageCount).toBe(2);
expect(docEdge!.properties.successRate).toBe(1.0);
});
it("should calculate average success rate correctly", async () => {
const manager = new UserPreferenceManager("avg-tracker");
await manager.initialize();
// Success
await manager.trackSSGUsage({
ssg: "mkdocs",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
// Failure
await manager.trackSSGUsage({
ssg: "mkdocs",
success: false,
timestamp: "2025-01-02T00:00:00.000Z",
});
const kg = await getKnowledgeGraph();
const edges = await kg.findEdges({
type: "user_prefers_ssg",
});
const mkdocsEdge = edges.find((e) => e.target.includes("mkdocs"));
expect(mkdocsEdge!.properties.successRate).toBe(0.5);
});
it("should create user node if it doesn't exist during tracking", async () => {
const manager = new UserPreferenceManager("new-tracker");
// Don't initialize - let trackSSGUsage create it
await manager.trackSSGUsage({
ssg: "eleventy",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
const kg = await getKnowledgeGraph();
const userNode = await kg.findNode({
type: "user",
properties: { userId: "new-tracker" },
});
expect(userNode).toBeDefined();
});
});
describe("SSG Recommendations", () => {
it("should return recommendations sorted by score", async () => {
const manager = new UserPreferenceManager("rec-test");
await manager.initialize();
// Track multiple SSGs with different success rates
await manager.trackSSGUsage({
ssg: "jekyll",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
await manager.trackSSGUsage({
ssg: "jekyll",
success: true,
timestamp: "2025-01-02T00:00:00.000Z",
});
await manager.trackSSGUsage({
ssg: "hugo",
success: true,
timestamp: "2025-01-03T00:00:00.000Z",
});
const recommendations = await manager.getSSGRecommendations();
expect(recommendations.length).toBeGreaterThan(0);
expect(recommendations[0].ssg).toBe("jekyll"); // Higher usage count
expect(recommendations[0].score).toBeGreaterThan(
recommendations[1].score,
);
});
it("should include reason with high success rate", async () => {
const manager = new UserPreferenceManager("reason-test");
await manager.initialize();
await manager.trackSSGUsage({
ssg: "docusaurus",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
const recommendations = await manager.getSSGRecommendations();
const docRec = recommendations.find((r) => r.ssg === "docusaurus");
expect(docRec!.reason).toContain("100% success rate");
});
it("should include reason with low success rate", async () => {
const manager = new UserPreferenceManager("low-success-test");
await manager.initialize();
// Track both success and failure to get a low rate (not exactly 0)
await manager.trackSSGUsage({
ssg: "eleventy",
success: true,
timestamp: "2025-01-01T00:00:00.000Z",
});
await manager.trackSSGUsage({
ssg: "eleventy",
success: false,
timestamp: "2025-01-02T00:00:00.000Z",
});
await manager.trackSSGUsage({
ssg: "eleventy",
success: false,
timestamp: "2025-01-03T00:00:00.000Z",
});
const recommendations = await manager.getSSGRecommendations();
const eleventyRec = recommendations.find((r) => r.ssg === "eleventy");
expect(eleventyRec!.reason).toContain("only");
expect(eleventyRec!.reason).toContain("success rate");
});
it("should return empty array if no user node exists", async () => {
const manager = new UserPreferenceManager("no-user");
// Don't initialize or create user node
const recommendations = await manager.getSSGRecommendations();
expect(recommendations).toEqual([]);
});
});
describe("Apply Preferences to Recommendation", () => {
it("should return original recommendation if autoApply is false", async () => {
const manager = new UserPreferenceManager("no-auto");
await manager.updatePreferences({
autoApplyPreferences: false,
preferredSSGs: ["jekyll"],
});
const result = manager.applyPreferencesToRecommendation("hugo", [
"jekyll",
"hugo",
]);
expect(result.recommended).toBe("hugo");
expect(result.adjustmentReason).toBeUndefined();
});
it("should keep recommendation if it matches preferred SSG", async () => {
const manager = new UserPreferenceManager("match-pref");
await manager.updatePreferences({
preferredSSGs: ["jekyll", "hugo"],
});
const result = manager.applyPreferencesToRecommendation("jekyll", [
"jekyll",
"hugo",
"mkdocs",
]);
expect(result.recommended).toBe("jekyll");
expect(result.adjustmentReason).toContain("Matches your preferred SSG");
});
it("should switch to preferred SSG if in alternatives", async () => {
const manager = new UserPreferenceManager("switch-pref");
await manager.updatePreferences({
preferredSSGs: ["docusaurus"],
});
const result = manager.applyPreferencesToRecommendation("jekyll", [
"jekyll",
"docusaurus",
"hugo",
]);
expect(result.recommended).toBe("docusaurus");
expect(result.adjustmentReason).toContain(
"Switched to docusaurus based on your usage history",
);
});
it("should return original if no preferred SSGs match", async () => {
const manager = new UserPreferenceManager("no-match");
await manager.updatePreferences({
preferredSSGs: ["eleventy"],
});
const result = manager.applyPreferencesToRecommendation("jekyll", [
"jekyll",
"hugo",
]);
expect(result.recommended).toBe("jekyll");
expect(result.adjustmentReason).toBeUndefined();
});
it("should return original if no preferences set", async () => {
const manager = new UserPreferenceManager("empty-pref");
await manager.initialize();
const result = manager.applyPreferencesToRecommendation("jekyll", [
"jekyll",
"hugo",
]);
expect(result.recommended).toBe("jekyll");
expect(result.adjustmentReason).toBeUndefined();
});
});
describe("Reset Preferences", () => {
it("should reset preferences to defaults", async () => {
const manager = new UserPreferenceManager("reset-test");
await manager.updatePreferences({
documentationStyle: "minimal",
expertiseLevel: "advanced",
preferredSSGs: ["jekyll", "hugo"],
});
await manager.resetPreferences();
const prefs = await manager.getPreferences();
expect(prefs.documentationStyle).toBe("comprehensive");
expect(prefs.expertiseLevel).toBe("intermediate");
expect(prefs.preferredSSGs).toEqual([]);
});
});
describe("Export/Import Preferences", () => {
it("should export preferences as JSON", async () => {
const manager = new UserPreferenceManager("export-test");
await manager.updatePreferences({
expertiseLevel: "advanced",
preferredSSGs: ["jekyll"],
});
const exported = await manager.exportPreferences();
const parsed = JSON.parse(exported);
expect(parsed.userId).toBe("export-test");
expect(parsed.expertiseLevel).toBe("advanced");
expect(parsed.preferredSSGs).toEqual(["jekyll"]);
});
it("should import preferences from JSON", async () => {
const manager = new UserPreferenceManager("import-test");
await manager.initialize();
const importData = {
userId: "import-test",
preferredSSGs: ["hugo", "docusaurus"],
documentationStyle: "tutorial-heavy" as const,
expertiseLevel: "beginner" as const,
preferredTechnologies: ["python"],
preferredDiataxisCategories: ["tutorials" as const],
autoApplyPreferences: false,
lastUpdated: "2025-01-01T00:00:00.000Z",
};
await manager.importPreferences(JSON.stringify(importData));
const prefs = await manager.getPreferences();
expect(prefs.expertiseLevel).toBe("beginner");
expect(prefs.preferredSSGs).toEqual(["hugo", "docusaurus"]);
expect(prefs.autoApplyPreferences).toBe(false);
});
it("should throw error on userId mismatch during import", async () => {
const manager = new UserPreferenceManager("user1");
await manager.initialize();
const importData = {
userId: "user2", // Different user ID
preferredSSGs: [],
documentationStyle: "comprehensive" as const,
expertiseLevel: "intermediate" as const,
preferredTechnologies: [],
preferredDiataxisCategories: [],
autoApplyPreferences: true,
lastUpdated: "2025-01-01T00:00:00.000Z",
};
await expect(
manager.importPreferences(JSON.stringify(importData)),
).rejects.toThrow("User ID mismatch");
});
});
describe("Manager Cache", () => {
it("should cache preference managers", async () => {
const manager1 = await getUserPreferenceManager("cached-user");
const manager2 = await getUserPreferenceManager("cached-user");
expect(manager1).toBe(manager2); // Same instance
});
it("should create different managers for different users", async () => {
const manager1 = await getUserPreferenceManager("user1");
const manager2 = await getUserPreferenceManager("user2");
expect(manager1).not.toBe(manager2);
});
it("should clear cache", async () => {
const manager1 = await getUserPreferenceManager("clear-test");
clearPreferenceManagerCache();
const manager2 = await getUserPreferenceManager("clear-test");
expect(manager1).not.toBe(manager2); // Different instances after clear
});
});
});
```
--------------------------------------------------------------------------------
/src/tools/optimize-readme.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { promises as fs } from "fs";
import path from "path";
import { MCPToolResponse } from "../types/api.js";
// Input validation schema
const OptimizeReadmeInputSchema = z.object({
readme_path: z.string().min(1, "README path is required"),
strategy: z
.enum([
"community_focused",
"enterprise_focused",
"developer_focused",
"general",
])
.optional()
.default("community_focused"),
max_length: z.number().min(50).max(1000).optional().default(300),
include_tldr: z.boolean().optional().default(true),
preserve_existing: z.boolean().optional().default(false),
output_path: z.string().optional(),
create_docs_directory: z.boolean().optional().default(true),
});
export type OptimizeReadmeInput = z.infer<typeof OptimizeReadmeInputSchema>;
interface OptimizationResult {
originalLength: number;
optimizedLength: number;
reductionPercentage: number;
optimizedContent: string;
extractedSections: ExtractedSection[];
tldrGenerated: string | null;
restructuringChanges: RestructuringChange[];
recommendations: string[];
}
interface ExtractedSection {
title: string;
content: string;
suggestedLocation: string;
reason: string;
}
interface RestructuringChange {
type: "moved" | "condensed" | "split" | "added" | "removed";
section: string;
description: string;
impact: string;
}
/**
* Optimizes README content by restructuring, condensing, and extracting detailed sections.
*
* Performs intelligent README optimization including length reduction, structure improvement,
* content extraction to separate documentation, and TL;DR generation. Uses different strategies
* based on target audience (community, enterprise, developer, general) to maximize effectiveness.
*
* @param input - The input parameters for README optimization
* @param input.readme_path - The file system path to the README file to optimize
* @param input.strategy - The optimization strategy to apply (default: "community_focused")
* @param input.max_length - Target maximum length in lines (default: 300)
* @param input.include_tldr - Whether to generate a TL;DR section (default: true)
* @param input.preserve_existing - Whether to preserve existing content structure (default: false)
* @param input.output_path - Optional output path for optimized README
* @param input.create_docs_directory - Whether to create docs/ directory for extracted content (default: true)
*
* @returns Promise resolving to README optimization results
* @returns optimization - Complete optimization results including length reduction and restructuring
* @returns nextSteps - Array of recommended next actions after optimization
*
* @throws {Error} When README file is inaccessible or invalid
* @throws {Error} When optimization processing fails
* @throws {Error} When output directory cannot be created
*
* @example
* ```typescript
* // Optimize README for community contributors
* const result = await optimizeReadme({
* readme_path: "./README.md",
* strategy: "community_focused",
* max_length: 300,
* include_tldr: true
* });
*
* console.log(`Reduced from ${result.data.optimization.originalLength} to ${result.data.optimization.optimizedLength} lines`);
* console.log(`Reduction: ${result.data.optimization.reductionPercentage}%`);
*
* // Optimize for enterprise with aggressive reduction
* const enterprise = await optimizeReadme({
* readme_path: "./README.md",
* strategy: "enterprise_focused",
* max_length: 200,
* preserve_existing: true
* });
* ```
*
* @since 1.0.0
*/
export async function optimizeReadme(
input: Partial<OptimizeReadmeInput>,
): Promise<
MCPToolResponse<{ optimization: OptimizationResult; nextSteps: string[] }>
> {
const startTime = Date.now();
try {
// Validate input
const validatedInput = OptimizeReadmeInputSchema.parse(input);
const {
readme_path,
strategy,
max_length,
include_tldr,
output_path,
create_docs_directory,
} = validatedInput;
// Read original README
const originalContent = await fs.readFile(readme_path, "utf-8");
const originalLength = originalContent.split("\n").length;
// Parse README structure
const sections = parseReadmeStructure(originalContent);
// Generate TL;DR if requested
const tldrGenerated = include_tldr
? generateTldr(originalContent, sections)
: null;
// Identify sections to extract
const extractedSections = identifySectionsToExtract(
sections,
strategy,
max_length,
);
// Create basic optimization result
const optimizedContent =
originalContent +
"\n\n## TL;DR\n\n" +
(tldrGenerated || "Quick overview of the project.");
const restructuringChanges = [
{
type: "added" as const,
section: "TL;DR",
description: "Added concise project overview",
impact: "Helps users quickly understand project value",
},
];
const optimizedLength = optimizedContent.split("\n").length;
const reductionPercentage = Math.round(
((originalLength - optimizedLength) / originalLength) * 100,
);
// Create docs directory and extract detailed content if requested
if (create_docs_directory && extractedSections.length > 0) {
await createDocsStructure(path.dirname(readme_path), extractedSections);
}
// Write optimized README if output path specified
if (output_path) {
await fs.writeFile(output_path, optimizedContent, "utf-8");
}
const recommendations = generateOptimizationRecommendations(
originalLength,
optimizedLength,
extractedSections,
strategy,
);
const optimization: OptimizationResult = {
originalLength,
optimizedLength,
reductionPercentage,
optimizedContent,
extractedSections,
tldrGenerated,
restructuringChanges,
recommendations,
};
const nextSteps = generateOptimizationNextSteps(
optimization,
validatedInput,
);
return {
success: true,
data: {
optimization,
nextSteps,
},
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
} catch (error) {
return {
success: false,
error: {
code: "OPTIMIZATION_FAILED",
message: "Failed to optimize README",
details: error instanceof Error ? error.message : "Unknown error",
resolution: "Check README file path and permissions",
},
metadata: {
toolVersion: "1.0.0",
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
},
};
}
}
interface ReadmeSection {
title: string;
content: string;
level: number;
startLine: number;
endLine: number;
wordCount: number;
isEssential: boolean;
}
function parseReadmeStructure(content: string): ReadmeSection[] {
const lines = content.split("\n");
const sections: ReadmeSection[] = [];
let currentTitle = "";
let currentLevel = 0;
let currentStartLine = 0;
lines.forEach((line, index) => {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
// Save previous section
if (currentTitle) {
const endLine = index - 1;
const sectionContent = lines
.slice(currentStartLine, endLine + 1)
.join("\n");
const wordCount = sectionContent.split(/\s+/).length;
const isEssential = isEssentialSection(currentTitle);
sections.push({
title: currentTitle,
content: sectionContent,
level: currentLevel,
startLine: currentStartLine,
endLine: endLine,
wordCount: wordCount,
isEssential: isEssential,
});
}
// Start new section
currentTitle = headingMatch[2].trim();
currentLevel = headingMatch[1].length;
currentStartLine = index;
}
});
// Add final section
if (currentTitle) {
const endLine = lines.length - 1;
const sectionContent = lines
.slice(currentStartLine, endLine + 1)
.join("\n");
const wordCount = sectionContent.split(/\s+/).length;
const isEssential = isEssentialSection(currentTitle);
sections.push({
title: currentTitle,
content: sectionContent,
level: currentLevel,
startLine: currentStartLine,
endLine: endLine,
wordCount: wordCount,
isEssential: isEssential,
});
}
return sections;
}
function isEssentialSection(title: string): boolean {
const essentialKeywords = [
"installation",
"install",
"setup",
"getting started",
"quick start",
"usage",
"example",
"api",
"license",
"contributing",
];
return essentialKeywords.some((keyword) =>
title.toLowerCase().includes(keyword),
);
}
function generateTldr(content: string, sections: ReadmeSection[]): string {
// Extract project name from first heading
const projectNameMatch = content.match(/^#\s+(.+)$/m);
const projectName = projectNameMatch ? projectNameMatch[1] : "This project";
// Extract description (usually after title or in blockquote)
const descriptionMatch = content.match(/>\s*(.+)/);
let description = descriptionMatch ? descriptionMatch[1] : "";
// If no description found, try to extract from first paragraph
if (!description) {
const firstParagraphMatch = content.match(/^[^#\n].{20,200}/m);
description = firstParagraphMatch
? firstParagraphMatch[0].substring(0, 100) + "..."
: "";
}
// Identify key features or use cases
const features: string[] = [];
sections.forEach((section) => {
if (
section.title.toLowerCase().includes("feature") ||
section.title.toLowerCase().includes("what") ||
section.title.toLowerCase().includes("why")
) {
const bullets = section.content.match(/^\s*[-*+]\s+(.+)$/gm);
if (bullets && bullets.length > 0) {
features.push(
...bullets
.slice(0, 3)
.map((b) => b.replace(/^\s*[-*+]\s+/, "").trim()),
);
}
}
});
let tldr = `## TL;DR\n\n${projectName} ${description}\n\n`;
if (features.length > 0) {
tldr += `**Key features:**\n`;
features.slice(0, 3).forEach((feature) => {
tldr += `- ${feature}\n`;
});
tldr += "\n";
}
// Add quick start reference
const hasInstallSection = sections.some(
(s) =>
s.title.toLowerCase().includes("install") ||
s.title.toLowerCase().includes("setup"),
);
if (hasInstallSection) {
tldr += `**Quick start:** See [Installation](#installation) → [Usage](#usage)\n\n`;
}
return tldr;
}
function identifySectionsToExtract(
sections: ReadmeSection[],
strategy: string,
maxLength: number,
): ExtractedSection[] {
const extractedSections: ExtractedSection[] = [];
const currentLength = sections.reduce(
(sum, s) => sum + s.content.split("\n").length,
0,
);
if (currentLength <= maxLength) {
return extractedSections; // No extraction needed
}
// Define extraction rules based on strategy
const extractionRules = getExtractionRules(strategy);
sections.forEach((section) => {
for (const rule of extractionRules) {
if (rule.matcher(section) && !section.isEssential) {
extractedSections.push({
title: section.title,
content: section.content,
suggestedLocation: rule.suggestedLocation,
reason: rule.reason,
});
break;
}
}
});
return extractedSections;
}
function getExtractionRules(strategy: string) {
const baseRules = [
{
matcher: (section: ReadmeSection) => section.wordCount > 200,
suggestedLocation: "docs/detailed-guide.md",
reason: "Section too long for main README",
},
{
matcher: (section: ReadmeSection) =>
/troubleshoot|faq|common issues|problems/i.test(section.title),
suggestedLocation: "docs/troubleshooting.md",
reason: "Troubleshooting content better suited for separate document",
},
{
matcher: (section: ReadmeSection) =>
/advanced|configuration|config/i.test(section.title),
suggestedLocation: "docs/configuration.md",
reason: "Advanced configuration details",
},
{
matcher: (section: ReadmeSection) =>
/development|developer|build|compile/i.test(section.title),
suggestedLocation: "docs/development.md",
reason: "Development-specific information",
},
];
if (strategy === "community_focused") {
baseRules.push({
matcher: (section: ReadmeSection) =>
/architecture|design|technical/i.test(section.title),
suggestedLocation: "docs/technical.md",
reason: "Technical details can overwhelm community contributors",
});
}
return baseRules;
}
async function createDocsStructure(
projectDir: string,
extractedSections: ExtractedSection[],
): Promise<void> {
const docsDir = path.join(projectDir, "docs");
try {
await fs.mkdir(docsDir, { recursive: true });
} catch {
// Directory might already exist
}
// Create extracted documentation files
for (const section of extractedSections) {
const filePath = path.join(projectDir, section.suggestedLocation);
const fileDir = path.dirname(filePath);
try {
await fs.mkdir(fileDir, { recursive: true });
await fs.writeFile(filePath, section.content, "utf-8");
} catch (error) {
console.warn(`Failed to create ${filePath}:`, error);
}
}
// Create docs index
const indexContent = generateDocsIndex(extractedSections);
await fs.writeFile(path.join(docsDir, "README.md"), indexContent, "utf-8");
}
function generateDocsIndex(extractedSections: ExtractedSection[]): string {
let content = "# Documentation\n\n";
content +=
"This directory contains detailed documentation extracted from the main README for better organization.\n\n";
content += "## Available Documentation\n\n";
extractedSections.forEach((section) => {
const filename = path.basename(section.suggestedLocation);
content += `- [${section.title}](${filename}) - ${section.reason}\n`;
});
return content;
}
function generateOptimizationRecommendations(
originalLength: number,
optimizedLength: number,
extractedSections: ExtractedSection[],
strategy: string,
): string[] {
const recommendations: string[] = [];
const reduction = originalLength - optimizedLength;
if (reduction > 0) {
recommendations.push(
`✅ Reduced README length by ${reduction} lines (${Math.round(
(reduction / originalLength) * 100,
)}%)`,
);
}
if (extractedSections.length > 0) {
recommendations.push(
`📁 Moved ${extractedSections.length} detailed sections to docs/ directory`,
);
}
if (strategy === "community_focused") {
recommendations.push(
"👥 Optimized for community contributors - prioritized quick start and contribution info",
);
}
recommendations.push(
"🔗 Added links to detailed documentation for users who need more information",
);
recommendations.push(
"📊 Consider adding a table of contents for sections with 5+ headings",
);
return recommendations;
}
function generateOptimizationNextSteps(
optimization: OptimizationResult,
input: OptimizeReadmeInput,
): string[] {
const steps: string[] = [];
if (!input.output_path) {
steps.push("💾 Review optimized content and save to README.md when ready");
}
if (optimization.extractedSections.length > 0) {
steps.push("📝 Review extracted documentation files in docs/ directory");
steps.push("🔗 Update any internal links that may have been affected");
}
if (optimization.reductionPercentage > 30) {
steps.push(
"👀 Have team members review the condensed content for accuracy",
);
}
steps.push("📈 Run analyze_readme again to verify improvements");
steps.push("🎯 Consider setting up automated README length monitoring");
return steps;
}
```