This is page 4 of 33. Use http://codebase.md/tosin2013/documcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
│ ├── agents
│ │ ├── documcp-ast.md
│ │ ├── documcp-deploy.md
│ │ ├── documcp-memory.md
│ │ ├── documcp-test.md
│ │ └── documcp-tool.md
│ ├── copilot-instructions.md
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── automated-changelog.md
│ │ ├── bug_report.md
│ │ ├── bug_report.yml
│ │ ├── documentation_issue.md
│ │ ├── feature_request.md
│ │ ├── feature_request.yml
│ │ ├── npm-publishing-fix.md
│ │ └── release_improvements.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── release-drafter.yml
│ └── workflows
│ ├── auto-merge.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── deploy-docs.yml
│ ├── README.md
│ ├── release-drafter.yml
│ └── release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .linkcheck.config.json
├── .markdown-link-check.json
├── .nvmrc
├── .pre-commit-config.yaml
├── .versionrc.json
├── ARCHITECTURAL_CHANGES_SUMMARY.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│ ├── .docusaurus
│ │ ├── docusaurus-plugin-content-docs
│ │ │ └── default
│ │ │ └── __mdx-loader-dependency.json
│ │ └── docusaurus-plugin-content-pages
│ │ └── default
│ │ └── __plugin.json
│ ├── adrs
│ │ ├── adr-0001-mcp-server-architecture.md
│ │ ├── adr-0002-repository-analysis-engine.md
│ │ ├── adr-0003-static-site-generator-recommendation-engine.md
│ │ ├── adr-0004-diataxis-framework-integration.md
│ │ ├── adr-0005-github-pages-deployment-automation.md
│ │ ├── adr-0006-mcp-tools-api-design.md
│ │ ├── adr-0007-mcp-prompts-and-resources-integration.md
│ │ ├── adr-0008-intelligent-content-population-engine.md
│ │ ├── adr-0009-content-accuracy-validation-framework.md
│ │ ├── adr-0010-mcp-resource-pattern-redesign.md
│ │ ├── adr-0011-ce-mcp-compatibility.md
│ │ ├── adr-0012-priority-scoring-system-for-documentation-drift.md
│ │ ├── adr-0013-release-pipeline-and-package-distribution.md
│ │ └── README.md
│ ├── api
│ │ ├── .nojekyll
│ │ ├── assets
│ │ │ ├── hierarchy.js
│ │ │ ├── highlight.css
│ │ │ ├── icons.js
│ │ │ ├── icons.svg
│ │ │ ├── main.js
│ │ │ ├── navigation.js
│ │ │ ├── search.js
│ │ │ └── style.css
│ │ ├── hierarchy.html
│ │ ├── index.html
│ │ ├── modules.html
│ │ └── variables
│ │ └── TOOLS.html
│ ├── assets
│ │ └── logo.svg
│ ├── CE-MCP-FINDINGS.md
│ ├── development
│ │ └── MCP_INSPECTOR_TESTING.md
│ ├── docusaurus.config.js
│ ├── explanation
│ │ ├── architecture.md
│ │ └── index.md
│ ├── guides
│ │ ├── link-validation.md
│ │ ├── playwright-integration.md
│ │ └── playwright-testing-workflow.md
│ ├── how-to
│ │ ├── analytics-setup.md
│ │ ├── change-watcher.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── drift-priority-scoring.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── llm-integration.md
│ │ ├── local-testing.md
│ │ ├── performance-optimization.md
│ │ ├── prompting-guide.md
│ │ ├── repository-analysis.md
│ │ ├── seo-optimization.md
│ │ ├── site-monitoring.md
│ │ ├── troubleshooting.md
│ │ └── usage-examples.md
│ ├── index.md
│ ├── knowledge-graph.md
│ ├── package-lock.json
│ ├── package.json
│ ├── phase-2-intelligence.md
│ ├── reference
│ │ ├── api-overview.md
│ │ ├── cli.md
│ │ ├── configuration.md
│ │ ├── deploy-pages.md
│ │ ├── index.md
│ │ ├── mcp-tools.md
│ │ └── prompt-templates.md
│ ├── research
│ │ ├── cross-domain-integration
│ │ │ └── README.md
│ │ ├── domain-1-mcp-architecture
│ │ │ ├── index.md
│ │ │ └── mcp-performance-research.md
│ │ ├── domain-2-repository-analysis
│ │ │ └── README.md
│ │ ├── domain-3-ssg-recommendation
│ │ │ ├── index.md
│ │ │ └── ssg-performance-analysis.md
│ │ ├── domain-4-diataxis-integration
│ │ │ └── README.md
│ │ ├── domain-5-github-deployment
│ │ │ ├── github-pages-security-analysis.md
│ │ │ └── index.md
│ │ ├── domain-6-api-design
│ │ │ └── README.md
│ │ ├── README.md
│ │ ├── research-integration-summary-2025-01-14.md
│ │ ├── research-progress-template.md
│ │ └── research-questions-2025-01-14.md
│ ├── robots.txt
│ ├── sidebars.js
│ ├── sitemap.xml
│ ├── src
│ │ └── css
│ │ └── custom.css
│ └── tutorials
│ ├── development-setup.md
│ ├── environment-setup.md
│ ├── first-deployment.md
│ ├── getting-started.md
│ ├── index.md
│ ├── memory-workflows.md
│ └── user-onboarding.md
├── ISSUE_IMPLEMENTATION_SUMMARY.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│ └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│ ├── benchmarks
│ │ └── performance.ts
│ ├── index.ts
│ ├── memory
│ │ ├── contextual-retrieval.ts
│ │ ├── deployment-analytics.ts
│ │ ├── enhanced-manager.ts
│ │ ├── export-import.ts
│ │ ├── freshness-kg-integration.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kg-code-integration.ts
│ │ ├── kg-health.ts
│ │ ├── kg-integration.ts
│ │ ├── kg-link-validator.ts
│ │ ├── kg-storage.ts
│ │ ├── knowledge-graph.ts
│ │ ├── learning.ts
│ │ ├── manager.ts
│ │ ├── multi-agent-sharing.ts
│ │ ├── pruning.ts
│ │ ├── schemas.ts
│ │ ├── storage.ts
│ │ ├── temporal-analysis.ts
│ │ ├── user-preferences.ts
│ │ └── visualization.ts
│ ├── prompts
│ │ └── technical-writer-prompts.ts
│ ├── scripts
│ │ └── benchmark.ts
│ ├── templates
│ │ └── playwright
│ │ ├── accessibility.spec.template.ts
│ │ ├── Dockerfile.template
│ │ ├── docs-e2e.workflow.template.yml
│ │ ├── link-validation.spec.template.ts
│ │ └── playwright.config.template.ts
│ ├── tools
│ │ ├── analyze-deployments.ts
│ │ ├── analyze-readme.ts
│ │ ├── analyze-repository.ts
│ │ ├── change-watcher.ts
│ │ ├── check-documentation-links.ts
│ │ ├── cleanup-agent-artifacts.ts
│ │ ├── deploy-pages.ts
│ │ ├── detect-gaps.ts
│ │ ├── evaluate-readme-health.ts
│ │ ├── generate-config.ts
│ │ ├── generate-contextual-content.ts
│ │ ├── generate-llm-context.ts
│ │ ├── generate-readme-template.ts
│ │ ├── generate-technical-writer-prompts.ts
│ │ ├── kg-health-check.ts
│ │ ├── manage-preferences.ts
│ │ ├── manage-sitemap.ts
│ │ ├── optimize-readme.ts
│ │ ├── populate-content.ts
│ │ ├── readme-best-practices.ts
│ │ ├── recommend-ssg.ts
│ │ ├── setup-playwright-tests.ts
│ │ ├── setup-structure.ts
│ │ ├── simulate-execution.ts
│ │ ├── sync-code-to-docs.ts
│ │ ├── test-local-deployment.ts
│ │ ├── track-documentation-freshness.ts
│ │ ├── update-existing-documentation.ts
│ │ ├── validate-content.ts
│ │ ├── validate-documentation-freshness.ts
│ │ ├── validate-readme-checklist.ts
│ │ └── verify-deployment.ts
│ ├── types
│ │ └── api.ts
│ ├── utils
│ │ ├── artifact-detector.ts
│ │ ├── ast-analyzer.ts
│ │ ├── change-watcher.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── execution-simulator.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── llm-client.ts
│ │ ├── permission-checker.ts
│ │ ├── semantic-analyzer.ts
│ │ ├── sitemap-generator.ts
│ │ ├── usage-metadata.ts
│ │ └── user-feedback-integration.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── call-graph-builder.test.ts
│ ├── change-watcher-priority.integration.test.ts
│ ├── change-watcher.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── execution-simulator.test.ts
│ ├── functional
│ │ └── tools.test.ts
│ ├── integration
│ │ ├── kg-documentation-workflow.test.ts
│ │ ├── knowledge-graph-workflow.test.ts
│ │ ├── mcp-readme-tools.test.ts
│ │ ├── memory-mcp-tools.test.ts
│ │ ├── readme-technical-writer.test.ts
│ │ └── workflow.test.ts
│ ├── memory
│ │ ├── contextual-retrieval.test.ts
│ │ ├── enhanced-manager.test.ts
│ │ ├── export-import.test.ts
│ │ ├── freshness-kg-integration.test.ts
│ │ ├── kg-code-integration.test.ts
│ │ ├── kg-health.test.ts
│ │ ├── kg-link-validator.test.ts
│ │ ├── kg-storage-validation.test.ts
│ │ ├── kg-storage.test.ts
│ │ ├── knowledge-graph-documentation-examples.test.ts
│ │ ├── knowledge-graph-enhanced.test.ts
│ │ ├── knowledge-graph.test.ts
│ │ ├── learning.test.ts
│ │ ├── manager-advanced.test.ts
│ │ ├── manager.test.ts
│ │ ├── mcp-resource-integration.test.ts
│ │ ├── mcp-tool-persistence.test.ts
│ │ ├── schemas-documentation-examples.test.ts
│ │ ├── schemas.test.ts
│ │ ├── storage.test.ts
│ │ ├── temporal-analysis.test.ts
│ │ └── user-preferences.test.ts
│ ├── performance
│ │ ├── memory-load-testing.test.ts
│ │ └── memory-stress-testing.test.ts
│ ├── prompts
│ │ ├── guided-workflow-prompts.test.ts
│ │ └── technical-writer-prompts.test.ts
│ ├── server.test.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── all-tools.test.ts
│ │ ├── analyze-coverage.test.ts
│ │ ├── analyze-deployments.test.ts
│ │ ├── analyze-readme.test.ts
│ │ ├── analyze-repository.test.ts
│ │ ├── check-documentation-links.test.ts
│ │ ├── cleanup-agent-artifacts.test.ts
│ │ ├── deploy-pages-kg-retrieval.test.ts
│ │ ├── deploy-pages-tracking.test.ts
│ │ ├── deploy-pages.test.ts
│ │ ├── detect-gaps.test.ts
│ │ ├── evaluate-readme-health.test.ts
│ │ ├── generate-contextual-content.test.ts
│ │ ├── generate-llm-context.test.ts
│ │ ├── generate-readme-template.test.ts
│ │ ├── generate-technical-writer-prompts.test.ts
│ │ ├── kg-health-check.test.ts
│ │ ├── manage-sitemap.test.ts
│ │ ├── optimize-readme.test.ts
│ │ ├── readme-best-practices.test.ts
│ │ ├── recommend-ssg-historical.test.ts
│ │ ├── recommend-ssg-preferences.test.ts
│ │ ├── recommend-ssg.test.ts
│ │ ├── simple-coverage.test.ts
│ │ ├── sync-code-to-docs.test.ts
│ │ ├── test-local-deployment.test.ts
│ │ ├── tool-error-handling.test.ts
│ │ ├── track-documentation-freshness.test.ts
│ │ ├── validate-content.test.ts
│ │ ├── validate-documentation-freshness.test.ts
│ │ └── validate-readme-checklist.test.ts
│ ├── types
│ │ └── type-safety.test.ts
│ └── utils
│ ├── artifact-detector.test.ts
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector-diataxis.test.ts
│ ├── drift-detector-priority.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ ├── llm-client.test.ts
│ ├── semantic-analyzer.test.ts
│ ├── sitemap-generator.test.ts
│ ├── usage-metadata.test.ts
│ └── user-feedback-integration.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
```markdown
1 | # DocuMCP AI Coding Agent Instructions
2 |
3 | DocuMCP is an intelligent MCP server for GitHub Pages documentation deployment with 45+ tools, Knowledge Graph memory system, and AST-based code analysis.
4 |
5 | ## Architecture Essentials
6 |
7 | ### MCP Server Design (src/index.ts)
8 |
9 | - **Stateless operation**: Each tool analyzes current state; no persistent session data
10 | - **Resource storage**: Temporary session context in `resourceStore` Map with URIs like `documcp://analysis/{id}`
11 | - **Response format**: ALL tools MUST use `formatMCPResponse()` from `src/types/api.ts` returning `MCPToolResponse`
12 | - **Memory integration**: Tools auto-store results in JSONL (`.documcp/memory/`) and query historical data
13 |
14 | ### Tool Implementation Pattern
15 |
16 | Every tool follows this structure:
17 |
18 | 1. Zod schema for input validation (e.g., `inputSchema.parse()`)
19 | 2. Business logic with error handling
20 | 3. Return via `formatMCPResponse({ success, data, metadata, recommendations?, nextSteps? })`
21 | 4. Store result as resource: `storeResourceFromToolResult(result, 'analysis', analysis.id)`
22 |
23 | **Example** (see `src/tools/analyze-repository.ts`):
24 |
25 | ```typescript
26 | const inputSchema = z.object({
27 | path: z.string(),
28 | depth: z.enum(["quick", "standard", "deep"]).optional(),
29 | });
30 | export async function analyzeRepository(
31 | args: unknown,
32 | ): Promise<MCPToolResponse> {
33 | const input = inputSchema.parse(args);
34 | // ... logic ...
35 | return formatMCPResponse({
36 | success: true,
37 | data: analysis,
38 | metadata: { toolVersion, executionTime, timestamp },
39 | });
40 | }
41 | ```
42 |
43 | ### Knowledge Graph Memory (src/memory/)
44 |
45 | - **Entities**: Project, User, Configuration, Technology, CodeFile, DocumentationSection (see `src/memory/schemas.ts`)
46 | - **Relationships**: `project_uses_technology`, `project_deployed_with`, `similar_to`, `documents`, etc.
47 | - **Storage**: `.documcp/memory/knowledge-graph-entities.jsonl` and `knowledge-graph-relationships.jsonl`
48 | - **Integration**: Use `createOrUpdateProject()`, `getProjectContext()`, `trackDeployment()` from `src/memory/kg-integration.ts`
49 |
50 | **analyze_repository enhancement**: Retrieves project history before analysis, shows previous analysis count, similar projects
51 |
52 | ## Development Workflows
53 |
54 | ### Essential Commands
55 |
56 | ```bash
57 | npm run build # Compile TypeScript → dist/
58 | npm run dev # Development watch mode with tsx
59 | npm test # Run Jest test suite
60 | npm run test:coverage # Coverage report (80% threshold)
61 | npm run build:inspect # Launch MCP Inspector for interactive testing
62 | npm run ci # Full CI pipeline: typecheck + lint + test + build
63 | make qa # Quality assurance: lint + types + test + coverage
64 | ```
65 |
66 | ### Testing Strategy
67 |
68 | - **Location**: `tests/` mirroring `src/` structure
69 | - **Coverage**: 80% global (branches/functions/lines), 60% for `recommend-ssg.ts` (complex logic)
70 | - **Excluded**: Experimental memory features, `src/index.ts` entry point
71 | - **Pattern**: Use `formatMCPResponse()` for consistent response validation
72 | - **Integration**: Multi-tool workflow tests in `tests/integration/`
73 |
74 | ### MCP Inspector Workflow
75 |
76 | 1. `npm run build:inspect` opens browser at `http://localhost:5173`
77 | 2. Click "Connect" to attach to server
78 | 3. Test tools with custom parameters interactively
79 | 4. Verify resources/prompts without full integration
80 |
81 | ## Project-Specific Conventions
82 |
83 | ### ESM Module Requirements
84 |
85 | - **ALL imports** must end with `.js` (even for `.ts` files): `import { foo } from './utils.js'`
86 | - Use `import type` for type-only imports: `import type { MyType } from './types.js'`
87 | - File URLs: `fileURLToPath(import.meta.url)` and `dirname()`
88 |
89 | ### Path & Permission Handling
90 |
91 | - **Security**: Use `isPathAllowed(path, allowedRoots)` from `src/utils/permission-checker.ts` before file operations
92 | - **Async FS**: Always use `fs.promises` API, never sync methods
93 | - **Cross-platform**: Use `path` module for joining/resolving
94 |
95 | ### Git Integration
96 |
97 | - Use `simple-git` library for repository operations
98 | - Handle missing `.git` directories gracefully (check `hasGit` flag)
99 | - Always validate repo state before analysis
100 |
101 | ### Diataxis Framework
102 |
103 | Documentation structured as:
104 |
105 | - **Tutorials**: Learning-oriented (getting started)
106 | - **How-To Guides**: Problem-solving (specific tasks)
107 | - **Reference**: Information-oriented (API docs)
108 | - **Explanation**: Understanding-oriented (architecture, concepts)
109 |
110 | Tools like `setup-structure` and `populate-content` enforce this structure.
111 |
112 | ## Critical Implementation Details
113 |
114 | ### Phase 3: AST-Based Code Analysis
115 |
116 | - **AST Parser** (`src/utils/ast-analyzer.ts`): Multi-language support via Tree-sitter (TypeScript, Python, Go, Rust, Java, Ruby, Bash)
117 | - **Drift Detection** (`src/utils/drift-detector.ts`): Snapshot-based comparison, categorizes changes (breaking/major/minor/patch)
118 | - **Sync Tool** (`src/tools/sync-code-to-docs.ts`): Modes: detect/preview/apply/auto, confidence threshold (default 0.8)
119 | - **Content Generator** (`src/tools/generate-contextual-content.ts`): Creates Diataxis-compliant docs from actual code structure
120 |
121 | **Example drift detection**:
122 |
123 | ```typescript
124 | // Detects function signature changes, new/removed classes, breaking changes
125 | const drift = await detectDrift({ projectPath, docsPath, snapshotDir });
126 | drift.affectedFiles.forEach((f) => console.log(f.driftType, f.severity));
127 | ```
128 |
129 | ### Error Handling Pattern
130 |
131 | ```typescript
132 | try {
133 | // tool logic
134 | return formatMCPResponse({ success: true, data: result, metadata });
135 | } catch (error) {
136 | return formatMCPResponse({
137 | success: false,
138 | error: {
139 | code: "TOOL_ERROR",
140 | message: error.message,
141 | resolution: "Check inputs and try again",
142 | },
143 | metadata,
144 | });
145 | }
146 | ```
147 |
148 | ### Adding New Tools Checklist
149 |
150 | 1. Create `src/tools/my-tool.ts` with Zod schema and logic
151 | 2. Export tool function returning `MCPToolResponse`
152 | 3. Add to `TOOLS` array in `src/index.ts` with name, description, inputSchema
153 | 4. Add handler in `CallToolRequestSchema` switch case
154 | 5. Store result: `storeResourceFromToolResult(result, 'type', id)`
155 | 6. Create tests: `tests/tools/my-tool.test.ts`
156 | 7. Run `npm run ci` to validate
157 |
158 | ## Key Integration Points
159 |
160 | ### Memory Query Patterns
161 |
162 | ```typescript
163 | // Query similar projects by analysis results
164 | const similar = await getSimilarProjects(analysisResult, limit);
165 |
166 | // Get project insights with historical context
167 | const insights = await getProjectInsights(projectPath);
168 |
169 | // Export/import memory for backup
170 | await exportMemories({ outputPath: "./backup.json" });
171 | await importMemories({ inputPath: "./backup.json" });
172 | ```
173 |
174 | ### Resource Storage Pattern
175 |
176 | ```typescript
177 | // Tools create resources for cross-tool reference
178 | resourceStore.set(`documcp://analysis/${id}`, {
179 | uri: `documcp://analysis/${id}`,
180 | name: `Repository Analysis ${id}`,
181 | mimeType: "application/json",
182 | text: JSON.stringify(result, null, 2),
183 | });
184 | ```
185 |
186 | ### ADR References
187 |
188 | - **ADR-001**: TypeScript MCP SDK chosen over Python/Go for ecosystem maturity
189 | - **ADR-002**: Multi-layered repository analysis (structure + dependencies + documentation)
190 | - **ADR-003**: Algorithmic SSG recommendation with confidence scoring
191 | - **ADR-006**: Consistent tool API design with Zod validation
192 | - **ADR-010**: Resource pattern redesign for session-based storage
193 |
194 | Full ADRs: `docs/adrs/`
195 |
196 | ## Common Pitfalls
197 |
198 | - ❌ Forgetting `.js` extension in imports → Module resolution fails
199 | - ❌ Using `formatMCPResponse()` incorrectly → Response validation errors
200 | - ❌ Missing Zod schema validation → Runtime type errors
201 | - ❌ Synchronous file operations → Blocking operations
202 | - ❌ Not checking path permissions → Security vulnerabilities
203 | - ❌ Returning raw objects instead of `MCPToolResponse` → Protocol violations
204 |
205 | ## Quick Reference
206 |
207 | **Node.js**: ≥20.0.0 | **Module**: ESM | **Test Framework**: Jest (ts-jest) | **Storage**: `.documcp/memory/` (JSONL)
208 | **MCP Inspector**: `npm run build:inspect` | **Full test suite**: `npm run ci` | **Docs**: `docs/` (Docusaurus)
209 |
```
--------------------------------------------------------------------------------
/docs/guides/link-validation.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | documcp:
3 | last_updated: "2025-11-20T00:46:21.947Z"
4 | last_validated: "2025-12-09T19:41:38.578Z"
5 | auto_updated: false
6 | update_frequency: monthly
7 | validated_against_commit: 306567b32114502c606244ad6c2930360bcd4201
8 | ---
9 |
10 | # Link Validation in Knowledge Graph
11 |
12 | ## Overview
13 |
14 | DocuMCP now includes automatic link validation for documentation content, integrated directly into the Knowledge Graph memory system. This feature validates external links, tracks their status over time, and surfaces broken links during repository analysis.
15 |
16 | ## Architecture
17 |
18 | ### Components
19 |
20 | 1. **kg-link-validator.ts** - Core link validation module
21 | 2. **kg-code-integration.ts** - Automatic validation during doc analysis
22 | 3. **Knowledge Graph** - Stores validation results as entities
23 |
24 | ### Entity Type: `link_validation`
25 |
26 | ```typescript
27 | {
28 | totalLinks: number; // Total links found
29 | validLinks: number; // Links that returned HTTP 200
30 | brokenLinks: number; // Links that failed (404, timeout, etc.)
31 | warningLinks: number; // Links that were skipped
32 | unknownLinks: number; // Links that couldn't be validated
33 | healthScore: number; // 0-100 score based on valid/total
34 | lastValidated: string; // ISO 8601 timestamp
35 | brokenLinksList: string[]; // Array of broken link URLs
36 | }
37 | ```
38 |
39 | ### Relationships
40 |
41 | 1. **has_link_validation**: `documentation_section` → `link_validation`
42 |
43 | - Connects docs to their validation results
44 |
45 | 2. **requires_fix**: `link_validation` → `documentation_section`
46 | - Created when broken links are detected
47 | - Properties:
48 | - `severity`: "high" (>5 broken) or "medium" (1-5 broken)
49 | - `brokenLinkCount`: Number of broken links
50 | - `detectedAt`: ISO timestamp
51 |
52 | ## How It Works
53 |
54 | ### 1. Automatic Validation During Analysis
55 |
56 | When `analyze_repository` runs, it:
57 |
58 | 1. Extracts documentation content
59 | 2. Creates documentation entities in KG
60 | 3. **Automatically validates external links** (async, non-blocking)
61 | 4. Stores validation results in KG
62 |
63 | ```typescript
64 | // In kg-code-integration.ts
65 | for (const doc of extractedContent.existingDocs) {
66 | const docNode = createDocSectionEntity(
67 | projectId,
68 | doc.path,
69 | doc.title,
70 | doc.content,
71 | );
72 | kg.addNode(docNode);
73 |
74 | // Validate links in background
75 | validateAndStoreDocumentationLinks(docNode.id, doc.content).catch((error) =>
76 | console.warn(`Failed to validate links: ${error.message}`),
77 | );
78 | }
79 | ```
80 |
81 | ### 2. Link Extraction
82 |
83 | Supports both Markdown and HTML formats:
84 |
85 | ```markdown
86 | <!-- Markdown links -->
87 |
88 | [GitHub](https://github.com)
89 |
90 | <!-- HTML links -->
91 |
92 | <a href="https://example.com">Link</a>
93 | ```
94 |
95 | ### 3. Validation Strategy
96 |
97 | Uses native Node.js `fetch` API with:
98 |
99 | - **HTTP HEAD requests** (faster than GET)
100 | - **5-second timeout** (configurable)
101 | - **Retry logic** (2 retries by default)
102 | - **Concurrent checking** (up to 10 simultaneous)
103 |
104 | ```typescript
105 | const result = await validateExternalLinks(urls, {
106 | timeout: 5000, // 5 seconds
107 | retryCount: 2, // Retry failed links
108 | concurrency: 10, // Check 10 links at once
109 | });
110 | ```
111 |
112 | ### 4. Storage in Knowledge Graph
113 |
114 | Results are stored as entities and can be queried:
115 |
116 | ```typescript
117 | // Get validation history for a doc section
118 | const history = await getLinkValidationHistory(docSectionId);
119 |
120 | // Latest validation
121 | const latest = history[0];
122 | console.log(`Health Score: ${latest.properties.healthScore}%`);
123 | console.log(`Broken Links: ${latest.properties.brokenLinks}`);
124 | ```
125 |
126 | ## Usage Examples
127 |
128 | ### Query Broken Links
129 |
130 | ```typescript
131 | import { getKnowledgeGraph } from "./memory/kg-integration.js";
132 |
133 | const kg = await getKnowledgeGraph();
134 |
135 | // Find all link validation entities with broken links
136 | const allNodes = await kg.getAllNodes();
137 | const validations = allNodes.filter(
138 | (n) => n.type === "link_validation" && n.properties.brokenLinks > 0,
139 | );
140 |
141 | validations.forEach((v) => {
142 | console.log(`Found ${v.properties.brokenLinks} broken links:`);
143 | v.properties.brokenLinksList.forEach((url) => console.log(` - ${url}`));
144 | });
145 | ```
146 |
147 | ### Get Documentation Health Report
148 |
149 | ```typescript
150 | import { getKnowledgeGraph } from "./memory/kg-integration.js";
151 |
152 | const kg = await getKnowledgeGraph();
153 |
154 | // Find all documentation sections
155 | const docSections = (await kg.getAllNodes()).filter(
156 | (n) => n.type === "documentation_section",
157 | );
158 |
159 | for (const doc of docSections) {
160 | // Get validation results
161 | const edges = await kg.findEdges({
162 | source: doc.id,
163 | type: "has_link_validation",
164 | });
165 |
166 | if (edges.length > 0) {
167 | const validationId = edges[0].target;
168 | const validation = (await kg.getAllNodes()).find(
169 | (n) => n.id === validationId,
170 | );
171 |
172 | if (validation) {
173 | console.log(`\n${doc.properties.filePath}:`);
174 | console.log(` Health: ${validation.properties.healthScore}%`);
175 | console.log(` Valid: ${validation.properties.validLinks}`);
176 | console.log(` Broken: ${validation.properties.brokenLinks}`);
177 | }
178 | }
179 | }
180 | ```
181 |
182 | ### Manual Validation
183 |
184 | ```typescript
185 | import {
186 | validateExternalLinks,
187 | storeLinkValidationInKG,
188 | } from "./memory/kg-link-validator.js";
189 |
190 | // Validate specific URLs
191 | const result = await validateExternalLinks([
192 | "https://github.com",
193 | "https://example.com/404",
194 | ]);
195 |
196 | console.log(result);
197 | // {
198 | // totalLinks: 2,
199 | // validLinks: 1,
200 | // brokenLinks: 1,
201 | // results: [...]
202 | // }
203 |
204 | // Store in KG
205 | await storeLinkValidationInKG(docSectionId, result);
206 | ```
207 |
208 | ## Integration with analyze_repository
209 |
210 | The `analyze_repository` tool now includes link validation data:
211 |
212 | ```json
213 | {
214 | "success": true,
215 | "data": {
216 | "intelligentAnalysis": {
217 | "documentationHealth": {
218 | "outdatedCount": 2,
219 | "coveragePercent": 85,
220 | "totalCodeFiles": 20,
221 | "documentedFiles": 17,
222 | "linkHealth": {
223 | "totalLinks": 45,
224 | "brokenLinks": 3,
225 | "healthScore": 93
226 | }
227 | }
228 | }
229 | }
230 | }
231 | ```
232 |
233 | ## Configuration
234 |
235 | ### Environment Variables
236 |
237 | ```bash
238 | # Link validation timeout (milliseconds)
239 | DOCUMCP_LINK_TIMEOUT=5000
240 |
241 | # Maximum retries for failed links
242 | DOCUMCP_LINK_RETRIES=2
243 |
244 | # Concurrent link checks
245 | DOCUMCP_LINK_CONCURRENCY=10
246 | ```
247 |
248 | ### Skip Link Validation
249 |
250 | Link validation is non-blocking and runs in the background. If it fails, it logs a warning but doesn't stop the analysis.
251 |
252 | ## Performance Considerations
253 |
254 | 1. **Non-blocking**: Validation runs asynchronously after doc entities are created
255 | 2. **Cached Results**: Results stored in KG, no re-validation on subsequent reads
256 | 3. **Concurrent Checking**: Validates up to 10 links simultaneously
257 | 4. **Smart Timeouts**: 5-second timeout prevents hanging on slow servers
258 |
259 | ## Troubleshooting
260 |
261 | ### Links Not Being Validated
262 |
263 | **Issue**: Documentation sections have no validation results
264 |
265 | **Check**:
266 |
267 | 1. Are there external links in the content?
268 | 2. Check console for warnings: `Failed to validate links in...`
269 | 3. Verify network connectivity
270 |
271 | ### False Positives
272 |
273 | **Issue**: Valid links marked as broken
274 |
275 | **Solutions**:
276 |
277 | 1. Increase timeout: Some servers respond slowly
278 | 2. Check if server blocks HEAD requests (rare)
279 | 3. Verify URL is publicly accessible (not behind auth)
280 |
281 | ### Memory Storage Not Updated
282 |
283 | **Issue**: KG doesn't show validation results
284 |
285 | **Check**:
286 |
287 | ```typescript
288 | import { saveKnowledgeGraph } from "./memory/kg-integration.js";
289 |
290 | // Manually save KG
291 | await saveKnowledgeGraph();
292 | ```
293 |
294 | ## Future Enhancements
295 |
296 | 1. **AST-based Internal Link Validation**
297 |
298 | - Verify internal file references exist
299 | - Check anchor links (`#section-id`)
300 |
301 | 2. **Link Health Trends**
302 |
303 | - Track link health over time
304 | - Alert on degrading link quality
305 |
306 | 3. **Batch Re-validation**
307 |
308 | - MCP tool to re-check all links
309 | - Scheduled validation for long-lived projects
310 |
311 | 4. **Link Recommendation**
312 | - Suggest fixing broken links
313 | - Recommend archive.org alternatives for dead links
314 |
315 | ## Dependencies
316 |
317 | - **linkinator** (v6.1.4) - Link validation library (installed)
318 | - **native fetch** - Node.js 20+ built-in HTTP client
319 |
320 | ## Related Documentation
321 |
322 | - [Architecture Decision Records](../adrs/)
323 | - [Phase 2: Intelligence & Learning System](../phase-2-intelligence.md)
324 | - [Memory Workflows Tutorial](../tutorials/memory-workflows.md)
325 |
```
--------------------------------------------------------------------------------
/src/memory/kg-link-validator.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Knowledge Graph Link Validator
3 | * Validates external and internal links in documentation and stores results in KG
4 | */
5 |
6 | import crypto from "crypto";
7 | import { getKnowledgeGraph } from "./kg-integration.js";
8 | import { GraphNode } from "./knowledge-graph.js";
9 |
10 | export interface LinkValidationResult {
11 | url: string;
12 | status: "valid" | "broken" | "warning" | "unknown";
13 | statusCode?: number;
14 | errorMessage?: string;
15 | lastChecked: string;
16 | responseTime?: number;
17 | }
18 |
19 | export interface LinkValidationSummary {
20 | totalLinks: number;
21 | validLinks: number;
22 | brokenLinks: number;
23 | warningLinks: number;
24 | unknownLinks: number;
25 | results: LinkValidationResult[];
26 | }
27 |
28 | /**
29 | * Validate external links in documentation content
30 | */
31 | export async function validateExternalLinks(
32 | urls: string[],
33 | options?: {
34 | timeout?: number;
35 | retryCount?: number;
36 | concurrency?: number;
37 | },
38 | ): Promise<LinkValidationSummary> {
39 | const _timeout = options?.timeout || 5000;
40 | // const _retryCount = options?.retryCount || 2;
41 | // const _concurrency = options?.concurrency || 10;
42 |
43 | const results: LinkValidationResult[] = [];
44 | const summary: LinkValidationSummary = {
45 | totalLinks: urls.length,
46 | validLinks: 0,
47 | brokenLinks: 0,
48 | warningLinks: 0,
49 | unknownLinks: 0,
50 | results: [],
51 | };
52 |
53 | // Create a temporary HTML file with all links to validate
54 | // const _tempHtml = `<html><body>${urls
55 | // .map((url) => `<a href="${url}">${url}</a>`)
56 | // .join("\n")}</body></html>`;
57 |
58 | try {
59 | // Use individual validation for now (linkinator API is complex)
60 | // TODO: Optimize with linkinator's batch checking in future
61 | for (const url of urls) {
62 | try {
63 | const result = await validateSingleLink(url, _timeout);
64 | results.push(result);
65 |
66 | if (result.status === "valid") summary.validLinks++;
67 | else if (result.status === "broken") summary.brokenLinks++;
68 | else if (result.status === "warning") summary.warningLinks++;
69 | else summary.unknownLinks++;
70 | } catch {
71 | results.push({
72 | url,
73 | status: "unknown",
74 | errorMessage: "Validation failed",
75 | lastChecked: new Date().toISOString(),
76 | });
77 | summary.unknownLinks++;
78 | }
79 | }
80 | } catch (error) {
81 | console.warn("Link validation error:", error);
82 | }
83 |
84 | summary.results = results;
85 | return summary;
86 | }
87 |
88 | /**
89 | * Validate a single link with HTTP HEAD request
90 | */
91 | async function validateSingleLink(
92 | url: string,
93 | timeout: number,
94 | ): Promise<LinkValidationResult> {
95 | const startTime = Date.now();
96 |
97 | try {
98 | const controller = new AbortController();
99 | const timeoutId = setTimeout(() => controller.abort(), timeout);
100 |
101 | const response = await fetch(url, {
102 | method: "HEAD",
103 | signal: controller.signal,
104 | redirect: "follow",
105 | });
106 |
107 | clearTimeout(timeoutId);
108 |
109 | const responseTime = Date.now() - startTime;
110 |
111 | if (response.ok) {
112 | return {
113 | url,
114 | status: "valid",
115 | statusCode: response.status,
116 | lastChecked: new Date().toISOString(),
117 | responseTime,
118 | };
119 | } else {
120 | return {
121 | url,
122 | status: "broken",
123 | statusCode: response.status,
124 | errorMessage: `HTTP ${response.status}: ${response.statusText}`,
125 | lastChecked: new Date().toISOString(),
126 | responseTime,
127 | };
128 | }
129 | } catch (error: any) {
130 | return {
131 | url,
132 | status: "broken",
133 | errorMessage: error.message || "Network error",
134 | lastChecked: new Date().toISOString(),
135 | };
136 | }
137 | }
138 |
139 | /**
140 | * Store link validation results in Knowledge Graph
141 | */
142 | export async function storeLinkValidationInKG(
143 | docSectionId: string,
144 | validationSummary: LinkValidationSummary,
145 | ): Promise<void> {
146 | const kg = await getKnowledgeGraph();
147 |
148 | // Create link validation entity
149 | const validationId = `link_validation:${crypto
150 | .randomBytes(8)
151 | .toString("hex")}`;
152 |
153 | const validationNode: GraphNode = {
154 | id: validationId,
155 | type: "link_validation",
156 | label: "Link Validation Result",
157 | properties: {
158 | totalLinks: validationSummary.totalLinks,
159 | validLinks: validationSummary.validLinks,
160 | brokenLinks: validationSummary.brokenLinks,
161 | warningLinks: validationSummary.warningLinks,
162 | unknownLinks: validationSummary.unknownLinks,
163 | healthScore:
164 | validationSummary.totalLinks > 0
165 | ? (validationSummary.validLinks / validationSummary.totalLinks) * 100
166 | : 100,
167 | lastValidated: new Date().toISOString(),
168 | brokenLinksList: validationSummary.results
169 | .filter((r) => r.status === "broken")
170 | .map((r) => r.url),
171 | },
172 | weight: 1.0,
173 | lastUpdated: new Date().toISOString(),
174 | };
175 |
176 | kg.addNode(validationNode);
177 |
178 | // Create relationship: documentation_section -> link_validation
179 | kg.addEdge({
180 | source: docSectionId,
181 | target: validationId,
182 | type: "has_link_validation",
183 | weight: 1.0,
184 | confidence: 1.0,
185 | properties: {
186 | validatedAt: new Date().toISOString(),
187 | hasBrokenLinks: validationSummary.brokenLinks > 0,
188 | needsAttention: validationSummary.brokenLinks > 0,
189 | },
190 | });
191 |
192 | // If there are broken links, create "requires_fix" edges
193 | if (validationSummary.brokenLinks > 0) {
194 | kg.addEdge({
195 | source: validationId,
196 | target: docSectionId,
197 | type: "requires_fix",
198 | weight: validationSummary.brokenLinks / validationSummary.totalLinks,
199 | confidence: 1.0,
200 | properties: {
201 | severity: validationSummary.brokenLinks > 5 ? "high" : "medium",
202 | brokenLinkCount: validationSummary.brokenLinks,
203 | detectedAt: new Date().toISOString(),
204 | },
205 | });
206 | }
207 | }
208 |
209 | /**
210 | * Get link validation history from Knowledge Graph
211 | */
212 | export async function getLinkValidationHistory(
213 | docSectionId: string,
214 | ): Promise<GraphNode[]> {
215 | const kg = await getKnowledgeGraph();
216 |
217 | const edges = await kg.findEdges({
218 | source: docSectionId,
219 | type: "has_link_validation",
220 | });
221 |
222 | const validationNodes: GraphNode[] = [];
223 | const allNodes = await kg.getAllNodes();
224 |
225 | for (const edge of edges) {
226 | const validationNode = allNodes.find((n) => n.id === edge.target);
227 | if (validationNode) {
228 | validationNodes.push(validationNode);
229 | }
230 | }
231 |
232 | // Sort by lastValidated (newest first)
233 | return validationNodes.sort(
234 | (a, b) =>
235 | new Date(b.properties.lastValidated).getTime() -
236 | new Date(a.properties.lastValidated).getTime(),
237 | );
238 | }
239 |
240 | /**
241 | * Extract links from documentation content
242 | */
243 | export function extractLinksFromContent(content: string): {
244 | externalLinks: string[];
245 | internalLinks: string[];
246 | } {
247 | const externalLinks: string[] = [];
248 | const internalLinks: string[] = [];
249 |
250 | // Markdown links: [text](url)
251 | const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
252 | let match;
253 |
254 | while ((match = markdownLinkRegex.exec(content)) !== null) {
255 | const url = match[2];
256 | if (url.startsWith("http://") || url.startsWith("https://")) {
257 | externalLinks.push(url);
258 | } else {
259 | internalLinks.push(url);
260 | }
261 | }
262 |
263 | // HTML links: <a href="url">
264 | const htmlLinkRegex = /<a\s+(?:[^>]*?\s+)?href=(["'])(.*?)\1/gi;
265 |
266 | while ((match = htmlLinkRegex.exec(content)) !== null) {
267 | const url = match[2];
268 | if (url.startsWith("http://") || url.startsWith("https://")) {
269 | externalLinks.push(url);
270 | } else {
271 | internalLinks.push(url);
272 | }
273 | }
274 |
275 | return {
276 | externalLinks: [...new Set(externalLinks)], // Remove duplicates
277 | internalLinks: [...new Set(internalLinks)],
278 | };
279 | }
280 |
281 | /**
282 | * Validate all links in a documentation section and store in KG
283 | */
284 | export async function validateAndStoreDocumentationLinks(
285 | docSectionId: string,
286 | content: string,
287 | ): Promise<LinkValidationSummary> {
288 | const { externalLinks } = extractLinksFromContent(content);
289 |
290 | if (externalLinks.length === 0) {
291 | return {
292 | totalLinks: 0,
293 | validLinks: 0,
294 | brokenLinks: 0,
295 | warningLinks: 0,
296 | unknownLinks: 0,
297 | results: [],
298 | };
299 | }
300 |
301 | const validationSummary = await validateExternalLinks(externalLinks);
302 | await storeLinkValidationInKG(docSectionId, validationSummary);
303 |
304 | return validationSummary;
305 | }
306 |
```
--------------------------------------------------------------------------------
/docs/reference/cli.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | documcp:
3 | last_updated: "2025-11-20T00:46:21.960Z"
4 | last_validated: "2025-12-09T19:41:38.590Z"
5 | auto_updated: false
6 | update_frequency: monthly
7 | validated_against_commit: 306567b32114502c606244ad6c2930360bcd4201
8 | ---
9 |
10 | # Command Line Interface
11 |
12 | DocuMCP primarily operates as an MCP server integrated with AI assistants, but it also provides command-line utilities for direct usage and debugging.
13 |
14 | ## MCP Server Usage
15 |
16 | The primary way to use DocuMCP is through MCP-compatible clients:
17 |
18 | ### Starting the MCP Server
19 |
20 | ```bash
21 | # Using npx (recommended)
22 | npx documcp
23 |
24 | # Using global installation
25 | documcp
26 |
27 | # Using Node.js directly
28 | node dist/index.js
29 | ```
30 |
31 | ### Server Information
32 |
33 | ```bash
34 | # Check version
35 | documcp --version
36 |
37 | # Show help
38 | documcp --help
39 |
40 | # Debug mode
41 | DEBUG=* documcp
42 | ```
43 |
44 | ## MCP Client Integration
45 |
46 | ### Claude Desktop Configuration
47 |
48 | Add to `claude_desktop_config.json`:
49 |
50 | ```json
51 | {
52 | "mcpServers": {
53 | "documcp": {
54 | "command": "npx",
55 | "args": ["documcp"],
56 | "env": {
57 | "DOCUMCP_STORAGE_DIR": "/path/to/storage"
58 | }
59 | }
60 | }
61 | }
62 | ```
63 |
64 | ### Environment Variables
65 |
66 | | Variable | Description | Default |
67 | | --------------------- | ------------------------ | ----------------- |
68 | | `DOCUMCP_STORAGE_DIR` | Memory storage directory | `.documcp/memory` |
69 | | `DEBUG` | Enable debug logging | `false` |
70 | | `NODE_ENV` | Node.js environment | `development` |
71 |
72 | ## Development Commands
73 |
74 | For development and testing:
75 |
76 | ### Build Commands
77 |
78 | ```bash
79 | # Build TypeScript
80 | npm run build
81 |
82 | # Build in watch mode
83 | npm run dev
84 |
85 | # Type checking
86 | npm run typecheck
87 | ```
88 |
89 | ### Testing Commands
90 |
91 | ```bash
92 | # Run all tests
93 | npm test
94 |
95 | # Run tests with coverage
96 | npm run test:coverage
97 |
98 | # Run performance benchmarks
99 | npm run test:performance
100 |
101 | # CI test run
102 | npm run test:ci
103 | ```
104 |
105 | ### Code Quality Commands
106 |
107 | ```bash
108 | # Lint code
109 | npm run lint
110 |
111 | # Fix linting issues
112 | npm run lint:fix
113 |
114 | # Format code
115 | npm run format
116 |
117 | # Check formatting
118 | npm run format:check
119 |
120 | # Full validation
121 | npm run validate:rules
122 | ```
123 |
124 | ### Documentation Commands
125 |
126 | ```bash
127 | # Check documentation links
128 | npm run docs:check-links
129 |
130 | # Check external links
131 | npm run docs:check-links:external
132 |
133 | # Check internal links only
134 | npm run docs:check-links:internal
135 |
136 | # Validate documentation structure
137 | npm run docs:validate
138 |
139 | # Complete documentation test
140 | npm run docs:test
141 | ```
142 |
143 | ### Security Commands
144 |
145 | ```bash
146 | # Check for vulnerabilities
147 | npm run security:check
148 |
149 | # Audit dependencies
150 | npm audit
151 |
152 | # Fix security issues
153 | npm audit fix
154 | ```
155 |
156 | ### Benchmark Commands
157 |
158 | ```bash
159 | # Run performance benchmarks
160 | npm run benchmark:run
161 |
162 | # Show current performance metrics
163 | npm run benchmark:current
164 |
165 | # Create benchmark configuration
166 | npm run benchmark:create-config
167 |
168 | # Show benchmark help
169 | npm run benchmark:help
170 | ```
171 |
172 | ## Tool Invocation via CLI
173 |
174 | While DocuMCP is designed for MCP integration, you can test tools via Node.js:
175 |
176 | ### Direct Tool Testing
177 |
178 | ```javascript
179 | // test-tool.js
180 | import { analyzeRepository } from "./dist/tools/analyze-repository.js";
181 |
182 | async function test() {
183 | const result = await analyzeRepository({
184 | path: process.cwd(),
185 | depth: "standard",
186 | });
187 | console.log(JSON.stringify(result, null, 2));
188 | }
189 |
190 | test().catch(console.error);
191 | ```
192 |
193 | ```bash
194 | # Run test
195 | node test-tool.js
196 | ```
197 |
198 | ### Tool-Specific Examples
199 |
200 | **Repository Analysis:**
201 |
202 | ```javascript
203 | import { analyzeRepository } from "./dist/tools/analyze-repository.js";
204 |
205 | const analysis = await analyzeRepository({
206 | path: "/path/to/repository",
207 | depth: "deep",
208 | });
209 | ```
210 |
211 | **SSG Recommendation:**
212 |
213 | ```javascript
214 | import { recommendSSG } from "./dist/tools/recommend-ssg.js";
215 |
216 | const recommendation = await recommendSSG({
217 | analysisId: "analysis_12345",
218 | preferences: {
219 | ecosystem: "javascript",
220 | priority: "features",
221 | },
222 | });
223 | ```
224 |
225 | **Configuration Generation:**
226 |
227 | ```javascript
228 | import { generateConfig } from "./dist/tools/generate-config.js";
229 |
230 | const config = await generateConfig({
231 | ssg: "docusaurus",
232 | projectName: "My Project",
233 | outputPath: "./docs",
234 | });
235 | ```
236 |
237 | ## Debugging
238 |
239 | ### Debug Modes
240 |
241 | Enable detailed logging:
242 |
243 | ```bash
244 | # All debug info
245 | DEBUG=* documcp
246 |
247 | # Specific modules
248 | DEBUG=documcp:* documcp
249 | DEBUG=documcp:analysis documcp
250 | DEBUG=documcp:memory documcp
251 | ```
252 |
253 | ### Log Levels
254 |
255 | DocuMCP supports different log levels:
256 |
257 | ```bash
258 | # Error only
259 | NODE_ENV=production documcp
260 |
261 | # Development (verbose)
262 | NODE_ENV=development documcp
263 |
264 | # Custom logging
265 | DEBUG=documcp:error,documcp:warn documcp
266 | ```
267 |
268 | ### Performance Debugging
269 |
270 | ```bash
271 | # Enable performance tracking
272 | DEBUG=documcp:perf documcp
273 |
274 | # Memory usage tracking
275 | DEBUG=documcp:memory documcp
276 |
277 | # Network requests
278 | DEBUG=documcp:http documcp
279 | ```
280 |
281 | ## Configuration Files
282 |
283 | ### Project-level Configuration
284 |
285 | Create `.documcprc.json` in your project:
286 |
287 | ```json
288 | {
289 | "storage": {
290 | "directory": ".documcp/memory",
291 | "maxEntries": 1000,
292 | "cleanupDays": 30
293 | },
294 | "analysis": {
295 | "defaultDepth": "standard",
296 | "excludePatterns": ["node_modules", ".git", "dist"]
297 | },
298 | "deployment": {
299 | "defaultBranch": "gh-pages",
300 | "verifyDeployment": true
301 | }
302 | }
303 | ```
304 |
305 | ### Global Configuration
306 |
307 | Create `~/.documcp/config.json`:
308 |
309 | ```json
310 | {
311 | "defaultPreferences": {
312 | "ecosystem": "any",
313 | "priority": "simplicity"
314 | },
315 | "github": {
316 | "defaultOrg": "your-username"
317 | },
318 | "memory": {
319 | "enableLearning": true,
320 | "shareAnonymousData": false
321 | }
322 | }
323 | ```
324 |
325 | ## Exit Codes
326 |
327 | DocuMCP uses standard exit codes:
328 |
329 | | Code | Meaning |
330 | | ---- | -------------------- |
331 | | 0 | Success |
332 | | 1 | General error |
333 | | 2 | Invalid arguments |
334 | | 3 | File system error |
335 | | 4 | Network error |
336 | | 5 | Configuration error |
337 | | 6 | Tool execution error |
338 |
339 | ## Scripting and Automation
340 |
341 | ### Batch Operations
342 |
343 | Create scripts for common workflows:
344 |
345 | ```bash
346 | #!/bin/bash
347 | # deploy-docs.sh
348 |
349 | set -e
350 |
351 | echo "Starting documentation deployment..."
352 |
353 | # Test locally first
354 | echo "Testing local build..."
355 | npm run docs:validate
356 |
357 | # Deploy via DocuMCP
358 | echo "Analyzing repository..."
359 | # Trigger MCP analysis through your client
360 |
361 | echo "Deployment complete!"
362 | ```
363 |
364 | ### CI/CD Integration
365 |
366 | DocuMCP can be used in CI/CD pipelines:
367 |
368 | ```yaml
369 | # .github/workflows/docs.yml
370 | name: Documentation
371 |
372 | on:
373 | push:
374 | branches: [main]
375 | paths: ["docs/**", "*.md"]
376 |
377 | jobs:
378 | deploy-docs:
379 | runs-on: ubuntu-latest
380 | steps:
381 | - uses: actions/checkout@v4
382 |
383 | - name: Setup Node.js
384 | uses: actions/setup-node@v4
385 | with:
386 | node-version: "20"
387 |
388 | - name: Install DocuMCP
389 | run: npm install -g documcp
390 |
391 | - name: Validate documentation
392 | run: |
393 | # Use DocuMCP validation tools
394 | npm run docs:validate
395 | ```
396 |
397 | ### Programmatic Usage
398 |
399 | For advanced integration:
400 |
401 | ```javascript
402 | // integration.js
403 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
404 | import { analyzeRepository } from "./dist/tools/analyze-repository.js";
405 | import { recommendSSG } from "./dist/tools/recommend-ssg.js";
406 | import { deployPages } from "./dist/tools/deploy-pages.js";
407 |
408 | class DocuMCPIntegration {
409 | async deployDocumentation(repoPath) {
410 | // Analyze
411 | const analysis = await analyzeRepository({
412 | path: repoPath,
413 | depth: "standard",
414 | });
415 |
416 | // Get recommendation
417 | const recommendation = await recommendSSG({
418 | analysisId: analysis.id,
419 | });
420 |
421 | // Deploy
422 | const deployment = await deployPages({
423 | repository: repoPath,
424 | ssg: recommendation.recommended,
425 | });
426 |
427 | return { analysis, recommendation, deployment };
428 | }
429 | }
430 | ```
431 |
432 | ## Troubleshooting CLI Issues
433 |
434 | ### Common Problems
435 |
436 | **Command not found:**
437 |
438 | ```bash
439 | # Check installation
440 | which documcp
441 | npm list -g documcp
442 |
443 | # Reinstall if needed
444 | npm uninstall -g documcp
445 | npm install -g documcp
446 | ```
447 |
448 | **Permission errors:**
449 |
450 | ```bash
451 | # Check permissions
452 | ls -la $(which documcp)
453 |
454 | # Fix permissions
455 | chmod +x $(which documcp)
456 | ```
457 |
458 | **Module resolution errors:**
459 |
460 | ```bash
461 | # Clear npm cache
462 | npm cache clean --force
463 |
464 | # Rebuild
465 | npm run build
466 | ```
467 |
468 | ### Getting Help
469 |
470 | ```bash
471 | # Show help
472 | documcp --help
473 |
474 | # Show version
475 | documcp --version
476 |
477 | # Contact support
478 | echo "Report issues: https://github.com/tosin2013/documcp/issues"
479 | ```
480 |
481 | For more detailed troubleshooting, see the [Troubleshooting Guide](../how-to/troubleshooting.md).
482 |
```
--------------------------------------------------------------------------------
/docs/tutorials/first-deployment.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | documcp:
3 | last_updated: "2025-11-20T00:46:21.971Z"
4 | last_validated: "2025-12-09T19:41:38.603Z"
5 | auto_updated: false
6 | update_frequency: monthly
7 | validated_against_commit: 306567b32114502c606244ad6c2930360bcd4201
8 | ---
9 |
10 | # Your First Documentation Deployment
11 |
12 | This tutorial walks you through deploying your first documentation site using DocuMCP, from analysis to live GitHub Pages deployment.
13 |
14 | ## What You'll Build
15 |
16 | By the end of this tutorial, you'll have:
17 |
18 | - A live documentation site on GitHub Pages
19 | - Automated deployment workflow
20 | - Professional Diataxis-structured content
21 | - Understanding of DocuMCP's deployment process
22 |
23 | ## Prerequisites
24 |
25 | - Completed the [Getting Started](getting-started.md) tutorial
26 | - GitHub repository with your code
27 | - Write access to the repository
28 | - GitHub Pages enabled in repository settings
29 |
30 | ## Step-by-Step Deployment
31 |
32 | ### Step 1: Complete Repository Analysis
33 |
34 | If you haven't already, analyze your repository:
35 |
36 | ```bash
37 | # Prompt to DocuMCP:
38 | "analyze my repository for documentation deployment"
39 | ```
40 |
41 | Expected output includes analysis ID (e.g., `analysis_xyz789`) that we'll use throughout the deployment.
42 |
43 | ### Step 2: Get Deployment-Optimized Recommendations
44 |
45 | Request recommendations specifically for deployment:
46 |
47 | ```bash
48 | # Prompt:
49 | "recommend the best static site generator for GitHub Pages deployment based on analysis_xyz789"
50 | ```
51 |
52 | DocuMCP will consider:
53 |
54 | - **GitHub Pages compatibility** (native Jekyll support vs. Actions required)
55 | - **Build time** (Hugo's speed vs. Docusaurus features)
56 | - **Maintenance overhead** (MkDocs simplicity vs. Eleventy flexibility)
57 |
58 | ### Step 3: Generate Deployment Configuration
59 |
60 | Create production-ready configuration:
61 |
62 | ```bash
63 | # For example, if Docusaurus was recommended:
64 | "generate Docusaurus configuration for production deployment to GitHub Pages"
65 | ```
66 |
67 | This creates:
68 |
69 | - **docusaurus.config.js**: Optimized for GitHub Pages
70 | - **package.json updates**: Required dependencies
71 | - **Build scripts**: Production build configuration
72 |
73 | ### Step 4: Set Up Documentation Structure
74 |
75 | Create comprehensive documentation structure:
76 |
77 | ```bash
78 | # Prompt:
79 | "set up Diataxis documentation structure for Docusaurus deployment"
80 | ```
81 |
82 | Creates organized folders:
83 |
84 | ```
85 | docs/
86 | ├── tutorials/ # Learning-oriented
87 | ├── how-to-guides/ # Problem-solving
88 | ├── reference/ # Information-oriented
89 | ├── explanation/ # Understanding-oriented
90 | └── index.md # Landing page
91 | ```
92 |
93 | ### Step 5: Populate with Initial Content
94 |
95 | Generate starter content based on your project:
96 |
97 | ```bash
98 | # Prompt:
99 | "populate the documentation structure with content based on my project analysis"
100 | ```
101 |
102 | DocuMCP creates:
103 |
104 | - **Project-specific tutorials** based on your codebase
105 | - **API documentation** extracted from your code
106 | - **Installation guides** tailored to your tech stack
107 | - **Configuration examples** using your actual project structure
108 |
109 | ### Step 6: Deploy to GitHub Pages
110 |
111 | Set up automated deployment:
112 |
113 | ```bash
114 | # Prompt:
115 | "deploy my Docusaurus documentation to GitHub Pages with automated workflow"
116 | ```
117 |
118 | This generates:
119 |
120 | - **.github/workflows/deploy.yml**: GitHub Actions workflow
121 | - **Optimized build process**: Cached dependencies, parallel builds
122 | - **Security configuration**: OIDC tokens, minimal permissions
123 |
124 | ### Step 7: Verify Deployment
125 |
126 | Check that everything is working:
127 |
128 | ```bash
129 | # Prompt:
130 | "verify my GitHub Pages deployment is working correctly"
131 | ```
132 |
133 | DocuMCP checks:
134 |
135 | - **Workflow status**: Build and deployment success
136 | - **Site accessibility**: Homepage loads correctly
137 | - **Navigation**: All sections are reachable
138 | - **Asset loading**: CSS, JS, images work properly
139 |
140 | ## Example: Complete TypeScript Library Deployment
141 |
142 | Here's a real example for a TypeScript library:
143 |
144 | ### 1. Analysis Results
145 |
146 | ```json
147 | {
148 | "id": "analysis_ts_lib_001",
149 | "structure": {
150 | "totalFiles": 47,
151 | "languages": { ".ts": 32, ".md": 5, ".json": 3 },
152 | "hasTests": true,
153 | "hasCI": true
154 | },
155 | "recommendations": {
156 | "primaryLanguage": "typescript",
157 | "projectType": "library"
158 | }
159 | }
160 | ```
161 |
162 | ### 2. Recommendation
163 |
164 | ```json
165 | {
166 | "recommended": "docusaurus",
167 | "confidence": 0.88,
168 | "reasoning": [
169 | "TypeScript ecosystem alignment",
170 | "Excellent API documentation support",
171 | "React component integration for examples"
172 | ]
173 | }
174 | ```
175 |
176 | ### 3. Generated Configuration
177 |
178 | **docusaurus.config.js**:
179 |
180 | ```javascript
181 | const config = {
182 | title: "TypeScript Library Docs",
183 | tagline: "Comprehensive API documentation",
184 | url: "https://yourusername.github.io",
185 | baseUrl: "/your-repo-name/",
186 |
187 | // GitHub Pages deployment config
188 | organizationName: "yourusername",
189 | projectName: "your-repo-name",
190 | deploymentBranch: "gh-pages",
191 | trailingSlash: false,
192 |
193 | presets: [
194 | [
195 | "classic",
196 | {
197 | docs: {
198 | routeBasePath: "/",
199 | sidebarPath: require.resolve("./sidebars.js"),
200 | },
201 | theme: {
202 | customCss: require.resolve("./src/css/custom.css"),
203 | },
204 | },
205 | ],
206 | ],
207 | };
208 | ```
209 |
210 | ### 4. GitHub Actions Workflow
211 |
212 | **.github/workflows/deploy.yml**:
213 |
214 | ```yaml
215 | name: Deploy Documentation
216 |
217 | on:
218 | push:
219 | branches: [main]
220 | paths: ["docs/**", "docusaurus.config.js"]
221 |
222 | permissions:
223 | contents: read
224 | pages: write
225 | id-token: write
226 |
227 | jobs:
228 | deploy:
229 | environment:
230 | name: github-pages
231 | url: ${{ steps.deployment.outputs.page_url }}
232 | runs-on: ubuntu-latest
233 | steps:
234 | - name: Checkout
235 | uses: actions/checkout@v4
236 |
237 | - name: Setup Node.js
238 | uses: actions/setup-node@v4
239 | with:
240 | node-version: "20"
241 | cache: "npm"
242 |
243 | - name: Install dependencies
244 | run: npm ci
245 |
246 | - name: Build documentation
247 | run: npm run build
248 |
249 | - name: Setup Pages
250 | uses: actions/configure-pages@v5
251 |
252 | - name: Upload artifact
253 | uses: actions/upload-pages-artifact@v4
254 | with:
255 | path: "./build"
256 |
257 | - name: Deploy to GitHub Pages
258 | id: deployment
259 | uses: actions/deploy-pages@v4
260 | ```
261 |
262 | ## Verification Checklist
263 |
264 | After deployment, verify:
265 |
266 | - [ ] **Site is live** at `https://yourusername.github.io/repository-name`
267 | - [ ] **All sections load** (Tutorials, How-to, Reference, Explanation)
268 | - [ ] **Search works** (if enabled)
269 | - [ ] **Mobile responsive** design
270 | - [ ] **Fast loading** (check Core Web Vitals)
271 | - [ ] **SEO optimized** (meta tags, sitemap)
272 |
273 | ## Common Deployment Issues
274 |
275 | ### Build Failures
276 |
277 | **Problem**: Workflow fails during build
278 | **Solution**:
279 |
280 | - Check Node.js version compatibility
281 | - Verify all dependencies are in package.json
282 | - Review build logs in Actions tab
283 |
284 | ### Page Not Found (404)
285 |
286 | **Problem**: Site shows 404 error
287 | **Solution**:
288 |
289 | - Verify `baseUrl` in config matches repository name
290 | - Check GitHub Pages source is set to GitHub Actions
291 | - Confirm deployment branch exists
292 |
293 | ### Assets Not Loading
294 |
295 | **Problem**: CSS/JS files return 404
296 | **Solution**:
297 |
298 | - Ensure `publicPath` is configured correctly
299 | - Check trailing slash configuration
300 | - Verify asset paths are relative
301 |
302 | ## Performance Optimization
303 |
304 | ### Build Speed
305 |
306 | - **Caching**: Enable npm cache in GitHub Actions
307 | - **Parallel builds**: Use appropriate number of workers
308 | - **Incremental builds**: Only rebuild changed files
309 |
310 | ### Site Performance
311 |
312 | - **Image optimization**: Compress and use modern formats
313 | - **Code splitting**: Load only necessary JavaScript
314 | - **CDN integration**: Use GitHub's CDN for assets
315 |
316 | ## Next Steps
317 |
318 | Now that you have a deployed documentation site:
319 |
320 | 1. **[Set up development workflow](development-setup.md)** for ongoing maintenance
321 | 2. **[Configure custom domain](../how-to/custom-domains.md)** (optional)
322 | 3. **[Set up monitoring](../how-to/site-monitoring.md)** for uptime tracking
323 | 4. **[Optimize for search](../how-to/seo-optimization.md)** engines
324 |
325 | ## Summary
326 |
327 | You've successfully:
328 | ✅ Analyzed your repository for deployment
329 | ✅ Generated production-ready configuration
330 | ✅ Set up professional documentation structure
331 | ✅ Deployed to GitHub Pages with automation
332 | ✅ Verified your live documentation site
333 |
334 | Your documentation is now live and will automatically update with each commit!
335 |
336 | ## Troubleshooting
337 |
338 | If you encounter issues:
339 |
340 | 1. Check the [troubleshooting guide](../how-to/troubleshooting.md)
341 | 2. Review GitHub Actions logs
342 | 3. Verify repository permissions
343 | 4. Confirm GitHub Pages settings
344 |
345 | Need help? Open an issue on the [DocuMCP repository](https://github.com/tosin2013/documcp/issues).
346 |
```
--------------------------------------------------------------------------------
/src/tools/setup-playwright-tests.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Setup Playwright E2E Tests Tool
3 | * Generates Playwright test configuration and files for user's documentation site
4 | */
5 |
6 | import { promises as fs } from "fs";
7 | import path from "path";
8 | import { z } from "zod";
9 | // Return type matches MCP tool response format
10 | type ToolResponse = {
11 | content: Array<{ type: "text"; text: string }>;
12 | isError?: boolean;
13 | };
14 | import { fileURLToPath } from "url";
15 | import { dirname } from "path";
16 |
17 | const __filename = fileURLToPath(import.meta.url);
18 | const __dirname = dirname(__filename);
19 |
20 | const inputSchema = z.object({
21 | repositoryPath: z.string().describe("Path to the documentation repository"),
22 | ssg: z.enum(["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"]),
23 | projectName: z.string().describe("Project name for tests"),
24 | mainBranch: z.string().optional().default("main"),
25 | includeAccessibilityTests: z.boolean().optional().default(true),
26 | includeDockerfile: z.boolean().optional().default(true),
27 | includeGitHubActions: z.boolean().optional().default(true),
28 | });
29 |
30 | interface SSGConfig {
31 | buildCommand: string;
32 | buildDir: string;
33 | port: number;
34 | packageDeps: Record<string, string>;
35 | }
36 |
37 | const SSG_CONFIGS: Record<string, SSGConfig> = {
38 | jekyll: {
39 | buildCommand: "bundle exec jekyll build",
40 | buildDir: "_site",
41 | port: 4000,
42 | packageDeps: {},
43 | },
44 | hugo: {
45 | buildCommand: "hugo",
46 | buildDir: "public",
47 | port: 1313,
48 | packageDeps: {},
49 | },
50 | docusaurus: {
51 | buildCommand: "npm run build",
52 | buildDir: "build",
53 | port: 3000,
54 | packageDeps: {
55 | "@docusaurus/core": "^3.0.0",
56 | "@docusaurus/preset-classic": "^3.0.0",
57 | },
58 | },
59 | mkdocs: {
60 | buildCommand: "mkdocs build",
61 | buildDir: "site",
62 | port: 8000,
63 | packageDeps: {},
64 | },
65 | eleventy: {
66 | buildCommand: "npx @11ty/eleventy",
67 | buildDir: "_site",
68 | port: 8080,
69 | packageDeps: {
70 | "@11ty/eleventy": "^2.0.0",
71 | },
72 | },
73 | };
74 |
75 | export async function setupPlaywrightTests(
76 | args: unknown,
77 | ): Promise<ToolResponse> {
78 | const {
79 | repositoryPath,
80 | ssg,
81 | projectName,
82 | mainBranch,
83 | includeAccessibilityTests,
84 | includeDockerfile,
85 | includeGitHubActions,
86 | } = inputSchema.parse(args);
87 |
88 | try {
89 | const config = SSG_CONFIGS[ssg];
90 | const templatesDir = path.join(__dirname, "../templates/playwright");
91 |
92 | // Create directories
93 | const testsDir = path.join(repositoryPath, "tests/e2e");
94 | await fs.mkdir(testsDir, { recursive: true });
95 |
96 | if (includeGitHubActions) {
97 | const workflowsDir = path.join(repositoryPath, ".github/workflows");
98 | await fs.mkdir(workflowsDir, { recursive: true });
99 | }
100 |
101 | // Read and process templates
102 | const filesCreated: string[] = [];
103 |
104 | // 1. Playwright config
105 | const configTemplate = await fs.readFile(
106 | path.join(templatesDir, "playwright.config.template.ts"),
107 | "utf-8",
108 | );
109 | const playwrightConfig = configTemplate.replace(
110 | /{{port}}/g,
111 | config.port.toString(),
112 | );
113 |
114 | await fs.writeFile(
115 | path.join(repositoryPath, "playwright.config.ts"),
116 | playwrightConfig,
117 | );
118 | filesCreated.push("playwright.config.ts");
119 |
120 | // 2. Link validation tests
121 | const linkTestTemplate = await fs.readFile(
122 | path.join(templatesDir, "link-validation.spec.template.ts"),
123 | "utf-8",
124 | );
125 | const linkTest = linkTestTemplate.replace(/{{projectName}}/g, projectName);
126 |
127 | await fs.writeFile(
128 | path.join(testsDir, "link-validation.spec.ts"),
129 | linkTest,
130 | );
131 | filesCreated.push("tests/e2e/link-validation.spec.ts");
132 |
133 | // 3. Accessibility tests (if enabled)
134 | if (includeAccessibilityTests) {
135 | const a11yTemplate = await fs.readFile(
136 | path.join(templatesDir, "accessibility.spec.template.ts"),
137 | "utf-8",
138 | );
139 |
140 | await fs.writeFile(
141 | path.join(testsDir, "accessibility.spec.ts"),
142 | a11yTemplate,
143 | );
144 | filesCreated.push("tests/e2e/accessibility.spec.ts");
145 | }
146 |
147 | // 4. Dockerfile (if enabled)
148 | if (includeDockerfile) {
149 | const dockerTemplate = await fs.readFile(
150 | path.join(templatesDir, "Dockerfile.template"),
151 | "utf-8",
152 | );
153 | const dockerfile = dockerTemplate
154 | .replace(/{{ssg}}/g, ssg)
155 | .replace(/{{buildCommand}}/g, config.buildCommand)
156 | .replace(/{{buildDir}}/g, config.buildDir);
157 |
158 | await fs.writeFile(
159 | path.join(repositoryPath, "Dockerfile.playwright"),
160 | dockerfile,
161 | );
162 | filesCreated.push("Dockerfile.playwright");
163 | }
164 |
165 | // 5. GitHub Actions workflow (if enabled)
166 | if (includeGitHubActions) {
167 | const workflowTemplate = await fs.readFile(
168 | path.join(templatesDir, "docs-e2e.workflow.template.yml"),
169 | "utf-8",
170 | );
171 | const workflow = workflowTemplate
172 | .replace(/{{mainBranch}}/g, mainBranch)
173 | .replace(/{{buildCommand}}/g, config.buildCommand)
174 | .replace(/{{buildDir}}/g, config.buildDir)
175 | .replace(/{{port}}/g, config.port.toString());
176 |
177 | await fs.writeFile(
178 | path.join(repositoryPath, ".github/workflows/docs-e2e-tests.yml"),
179 | workflow,
180 | );
181 | filesCreated.push(".github/workflows/docs-e2e-tests.yml");
182 | }
183 |
184 | // 6. Update package.json
185 | const packageJsonPath = path.join(repositoryPath, "package.json");
186 | let packageJson: any = {};
187 |
188 | try {
189 | const existing = await fs.readFile(packageJsonPath, "utf-8");
190 | packageJson = JSON.parse(existing);
191 | } catch {
192 | // Create new package.json
193 | packageJson = {
194 | name: projectName.toLowerCase().replace(/\s+/g, "-"),
195 | version: "1.0.0",
196 | private: true,
197 | scripts: {},
198 | dependencies: {},
199 | devDependencies: {},
200 | };
201 | }
202 |
203 | // Add Playwright dependencies
204 | packageJson.devDependencies = {
205 | ...packageJson.devDependencies,
206 | "@playwright/test": "^1.55.1",
207 | ...(includeAccessibilityTests
208 | ? { "@axe-core/playwright": "^4.10.2" }
209 | : {}),
210 | };
211 |
212 | // Add test scripts
213 | packageJson.scripts = {
214 | ...packageJson.scripts,
215 | "test:e2e": "playwright test",
216 | "test:e2e:ui": "playwright test --ui",
217 | "test:e2e:report": "playwright show-report",
218 | "test:e2e:docker":
219 | "docker build -t docs-test -f Dockerfile.playwright . && docker run --rm docs-test",
220 | };
221 |
222 | await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
223 | filesCreated.push("package.json (updated)");
224 |
225 | // 7. Create .gitignore entries
226 | const gitignorePath = path.join(repositoryPath, ".gitignore");
227 | const gitignoreEntries = [
228 | "test-results/",
229 | "playwright-report/",
230 | "playwright-results.json",
231 | "playwright/.cache/",
232 | ].join("\n");
233 |
234 | try {
235 | const existing = await fs.readFile(gitignorePath, "utf-8");
236 | if (!existing.includes("test-results/")) {
237 | await fs.writeFile(
238 | gitignorePath,
239 | `${existing}\n\n# Playwright\n${gitignoreEntries}\n`,
240 | );
241 | filesCreated.push(".gitignore (updated)");
242 | }
243 | } catch {
244 | await fs.writeFile(gitignorePath, `# Playwright\n${gitignoreEntries}\n`);
245 | filesCreated.push(".gitignore");
246 | }
247 |
248 | return {
249 | content: [
250 | {
251 | type: "text" as const,
252 | text: JSON.stringify(
253 | {
254 | success: true,
255 | filesCreated,
256 | nextSteps: [
257 | "Run `npm install` to install Playwright dependencies",
258 | "Run `npx playwright install` to download browser binaries",
259 | "Test locally: `npm run test:e2e`",
260 | includeDockerfile
261 | ? "Build container: `docker build -t docs-test -f Dockerfile.playwright .`"
262 | : "",
263 | includeGitHubActions
264 | ? "Push to trigger GitHub Actions workflow"
265 | : "",
266 | ].filter(Boolean),
267 | configuration: {
268 | ssg,
269 | buildCommand: config.buildCommand,
270 | buildDir: config.buildDir,
271 | port: config.port,
272 | testsIncluded: {
273 | linkValidation: true,
274 | accessibility: includeAccessibilityTests,
275 | },
276 | integrations: {
277 | docker: includeDockerfile,
278 | githubActions: includeGitHubActions,
279 | },
280 | },
281 | },
282 | null,
283 | 2,
284 | ),
285 | },
286 | ],
287 | };
288 | } catch (error: any) {
289 | return {
290 | content: [
291 | {
292 | type: "text" as const,
293 | text: JSON.stringify(
294 | {
295 | success: false,
296 | error: error.message,
297 | },
298 | null,
299 | 2,
300 | ),
301 | },
302 | ],
303 | isError: true,
304 | };
305 | }
306 | }
307 |
```
--------------------------------------------------------------------------------
/src/tools/kg-health-check.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Knowledge Graph Health Check Tool
3 | * MCP tool for checking knowledge graph health and getting recommendations
4 | */
5 |
6 | import { z } from "zod";
7 | import { MCPToolResponse, formatMCPResponse } from "../types/api.js";
8 | import { getKnowledgeGraph, getKGStorage } from "../memory/kg-integration.js";
9 | import { KGHealthMonitor, KGHealthMetrics } from "../memory/kg-health.js";
10 |
11 | const inputSchema = z.object({
12 | includeHistory: z.boolean().optional().default(false),
13 | generateReport: z.boolean().optional().default(true),
14 | days: z.number().min(1).max(90).optional().default(7),
15 | });
16 |
17 | /**
18 | * Check the health of the knowledge graph
19 | *
20 | * Performs comprehensive health analysis including data quality, structure health,
21 | * performance metrics, issue detection, and trend analysis.
22 | *
23 | * @param args - The input arguments
24 | * @param args.includeHistory - Include historical health trend data
25 | * @param args.generateReport - Generate a formatted health report
26 | * @param args.days - Number of days of history to include (1-90)
27 | *
28 | * @returns Health metrics with recommendations
29 | *
30 | * @example
31 | * ```typescript
32 | * const result = await checkKGHealth({
33 | * includeHistory: true,
34 | * generateReport: true,
35 | * days: 7
36 | * });
37 | * ```
38 | */
39 | export async function checkKGHealth(
40 | args: unknown,
41 | ): Promise<{ content: any[]; isError?: boolean }> {
42 | const startTime = Date.now();
43 |
44 | try {
45 | const { includeHistory, generateReport } = inputSchema.parse(args);
46 |
47 | // Get KG instances
48 | const kg = await getKnowledgeGraph();
49 | const storage = await getKGStorage();
50 |
51 | // Create health monitor
52 | const monitor = new KGHealthMonitor();
53 |
54 | // Calculate health
55 | const health = await monitor.calculateHealth(kg, storage);
56 |
57 | // Generate report if requested
58 | let report = "";
59 | if (generateReport) {
60 | report = generateHealthReport(health, includeHistory);
61 | }
62 |
63 | const response: MCPToolResponse<KGHealthMetrics> = {
64 | success: true,
65 | data: health,
66 | metadata: {
67 | toolVersion: "1.0.0",
68 | executionTime: Date.now() - startTime,
69 | timestamp: new Date().toISOString(),
70 | },
71 | recommendations: health.recommendations.map((rec) => ({
72 | type: rec.priority === "high" ? "warning" : "info",
73 | title: rec.action,
74 | description: `Expected impact: +${rec.expectedImpact} health score | Effort: ${rec.effort}`,
75 | })),
76 | nextSteps: [
77 | {
78 | action: "Apply Recommendations",
79 | toolRequired: "manual",
80 | description:
81 | "Implement high-priority recommendations to improve health",
82 | priority: "high",
83 | },
84 | ...(health.issues.filter((i) => i.severity === "critical").length > 0
85 | ? [
86 | {
87 | action: "Fix Critical Issues",
88 | toolRequired: "manual" as const,
89 | description: "Address critical issues immediately",
90 | priority: "high" as const,
91 | },
92 | ]
93 | : []),
94 | ],
95 | };
96 |
97 | if (generateReport) {
98 | // Add report as additional content
99 | return {
100 | content: [
101 | ...formatMCPResponse(response).content,
102 | {
103 | type: "text",
104 | text: report,
105 | },
106 | ],
107 | };
108 | }
109 |
110 | return formatMCPResponse(response);
111 | } catch (error) {
112 | const errorResponse: MCPToolResponse = {
113 | success: false,
114 | error: {
115 | code: "HEALTH_CHECK_FAILED",
116 | message: `Failed to check KG health: ${error}`,
117 | resolution: "Ensure the knowledge graph is properly initialized",
118 | },
119 | metadata: {
120 | toolVersion: "1.0.0",
121 | executionTime: Date.now() - startTime,
122 | timestamp: new Date().toISOString(),
123 | },
124 | };
125 | return formatMCPResponse(errorResponse);
126 | }
127 | }
128 |
129 | /**
130 | * Generate a human-readable health report
131 | */
132 | function generateHealthReport(
133 | health: KGHealthMetrics,
134 | includeHistory: boolean,
135 | ): string {
136 | const lines: string[] = [];
137 |
138 | // Header
139 | lines.push("═══════════════════════════════════════════════════════");
140 | lines.push(" KNOWLEDGE GRAPH HEALTH REPORT");
141 | lines.push("═══════════════════════════════════════════════════════");
142 | lines.push("");
143 |
144 | // Overall Health
145 | lines.push(
146 | `📊 OVERALL HEALTH: ${health.overallHealth}/100 ${getHealthEmoji(
147 | health.overallHealth,
148 | )}`,
149 | );
150 | lines.push(
151 | ` Trend: ${health.trends.healthTrend.toUpperCase()} ${getTrendEmoji(
152 | health.trends.healthTrend,
153 | )}`,
154 | );
155 | lines.push("");
156 |
157 | // Component Scores
158 | lines.push("Component Scores:");
159 | lines.push(
160 | ` • Data Quality: ${health.dataQuality.score}/100 ${getHealthEmoji(
161 | health.dataQuality.score,
162 | )}`,
163 | );
164 | lines.push(
165 | ` • Structure Health: ${health.structureHealth.score}/100 ${getHealthEmoji(
166 | health.structureHealth.score,
167 | )}`,
168 | );
169 | lines.push(
170 | ` • Performance: ${health.performance.score}/100 ${getHealthEmoji(
171 | health.performance.score,
172 | )}`,
173 | );
174 | lines.push("");
175 |
176 | // Graph Statistics
177 | lines.push("Graph Statistics:");
178 | lines.push(` • Total Nodes: ${health.dataQuality.totalNodes}`);
179 | lines.push(` • Total Edges: ${health.dataQuality.totalEdges}`);
180 | lines.push(
181 | ` • Avg Connectivity: ${health.structureHealth.densityScore.toFixed(3)}`,
182 | );
183 | lines.push(
184 | ` • Storage Size: ${formatBytes(health.performance.storageSize)}`,
185 | );
186 | lines.push("");
187 |
188 | // Data Quality Details
189 | if (health.dataQuality.score < 90) {
190 | lines.push("⚠️ Data Quality Issues:");
191 | if (health.dataQuality.staleNodeCount > 0) {
192 | lines.push(
193 | ` • ${health.dataQuality.staleNodeCount} stale nodes (>30 days old)`,
194 | );
195 | }
196 | if (health.dataQuality.orphanedEdgeCount > 0) {
197 | lines.push(` • ${health.dataQuality.orphanedEdgeCount} orphaned edges`);
198 | }
199 | if (health.dataQuality.duplicateCount > 0) {
200 | lines.push(` • ${health.dataQuality.duplicateCount} duplicate entities`);
201 | }
202 | if (health.dataQuality.completenessScore < 0.8) {
203 | lines.push(
204 | ` • Completeness: ${Math.round(
205 | health.dataQuality.completenessScore * 100,
206 | )}%`,
207 | );
208 | }
209 | lines.push("");
210 | }
211 |
212 | // Critical Issues
213 | const criticalIssues = health.issues.filter((i) => i.severity === "critical");
214 | const highIssues = health.issues.filter((i) => i.severity === "high");
215 |
216 | if (criticalIssues.length > 0 || highIssues.length > 0) {
217 | lines.push("🚨 CRITICAL & HIGH PRIORITY ISSUES:");
218 | for (const issue of [...criticalIssues, ...highIssues].slice(0, 5)) {
219 | lines.push(` [${issue.severity.toUpperCase()}] ${issue.description}`);
220 | lines.push(` → ${issue.remediation}`);
221 | }
222 | lines.push("");
223 | }
224 |
225 | // Top Recommendations
226 | if (health.recommendations.length > 0) {
227 | lines.push("💡 TOP RECOMMENDATIONS:");
228 | for (const rec of health.recommendations.slice(0, 5)) {
229 | lines.push(` ${getPriorityIcon(rec.priority)} ${rec.action}`);
230 | lines.push(` Impact: +${rec.expectedImpact} | Effort: ${rec.effort}`);
231 | }
232 | lines.push("");
233 | }
234 |
235 | // Trends
236 | if (includeHistory) {
237 | lines.push("📈 TRENDS (Last 7 Days):");
238 | lines.push(
239 | ` • Health: ${health.trends.healthTrend} ${getTrendEmoji(
240 | health.trends.healthTrend,
241 | )}`,
242 | );
243 | lines.push(
244 | ` • Quality: ${health.trends.qualityTrend} ${getTrendEmoji(
245 | health.trends.qualityTrend,
246 | )}`,
247 | );
248 | lines.push(
249 | ` • Node Growth: ${health.trends.nodeGrowthRate.toFixed(1)} nodes/day`,
250 | );
251 | lines.push(
252 | ` • Edge Growth: ${health.trends.edgeGrowthRate.toFixed(1)} edges/day`,
253 | );
254 | lines.push("");
255 | }
256 |
257 | // Footer
258 | lines.push("═══════════════════════════════════════════════════════");
259 | lines.push(
260 | `Report generated: ${new Date(health.timestamp).toLocaleString()}`,
261 | );
262 | lines.push("═══════════════════════════════════════════════════════");
263 |
264 | return lines.join("\n");
265 | }
266 |
267 | // Helper functions
268 |
269 | function getHealthEmoji(score: number): string {
270 | if (score >= 90) return "🟢 Excellent";
271 | if (score >= 75) return "🟡 Good";
272 | if (score >= 60) return "🟠 Fair";
273 | return "🔴 Poor";
274 | }
275 |
276 | function getTrendEmoji(trend: string): string {
277 | if (trend === "improving") return "📈";
278 | if (trend === "degrading") return "📉";
279 | return "➡️";
280 | }
281 |
282 | function getPriorityIcon(priority: string): string {
283 | if (priority === "high") return "🔴";
284 | if (priority === "medium") return "🟡";
285 | return "🟢";
286 | }
287 |
288 | function formatBytes(bytes: number): string {
289 | if (bytes < 1024) return `${bytes} B`;
290 | if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
291 | if (bytes < 1024 * 1024 * 1024)
292 | return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
293 | return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
294 | }
295 |
```
--------------------------------------------------------------------------------
/tests/tools/kg-health-check.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from "fs";
2 | import path from "path";
3 | import os from "os";
4 | import { checkKGHealth } from "../../src/tools/kg-health-check";
5 | import {
6 | getKnowledgeGraph,
7 | getKGStorage,
8 | createOrUpdateProject,
9 | } from "../../src/memory/kg-integration";
10 |
11 | describe("KG Health Check Tool", () => {
12 | let tempDir: string;
13 | const originalCwd = process.cwd();
14 |
15 | beforeEach(async () => {
16 | tempDir = path.join(os.tmpdir(), `kg-health-${Date.now()}`);
17 | await fs.mkdir(tempDir, { recursive: true });
18 | process.chdir(tempDir);
19 | });
20 |
21 | afterEach(async () => {
22 | process.chdir(originalCwd);
23 | try {
24 | await fs.rm(tempDir, { recursive: true, force: true });
25 | } catch {
26 | // Ignore cleanup errors
27 | }
28 | });
29 |
30 | it("should perform basic health check", async () => {
31 | const result = await checkKGHealth({
32 | includeHistory: false,
33 | generateReport: false,
34 | });
35 |
36 | expect(result.content).toBeDefined();
37 | expect(result.content.length).toBeGreaterThan(0);
38 |
39 | // Should contain health metrics
40 | const text = result.content.map((c) => c.text).join(" ");
41 | expect(text).toContain("Health");
42 | });
43 |
44 | it("should include historical data when requested", async () => {
45 | const result = await checkKGHealth({
46 | includeHistory: true,
47 | generateReport: false,
48 | days: 7,
49 | });
50 |
51 | expect(result.content).toBeDefined();
52 | const text = result.content.map((c) => c.text).join(" ");
53 | expect(text).toContain("Health");
54 | });
55 |
56 | it("should generate detailed report", async () => {
57 | const result = await checkKGHealth({
58 | includeHistory: false,
59 | generateReport: true,
60 | });
61 |
62 | expect(result.content).toBeDefined();
63 | expect(result.content.length).toBeGreaterThan(0);
64 |
65 | // Report should contain detailed metrics
66 | const text = result.content.map((c) => c.text).join(" ");
67 | expect(text).toContain("Health");
68 | });
69 |
70 | it("should generate report with history included", async () => {
71 | const result = await checkKGHealth({
72 | includeHistory: true,
73 | generateReport: true,
74 | days: 14,
75 | });
76 |
77 | expect(result.content).toBeDefined();
78 | expect(result.content.length).toBeGreaterThan(1); // Should have formatted response + report
79 |
80 | const text = result.content.map((c) => c.text).join(" ");
81 | expect(text).toContain("Health");
82 | expect(text).toContain("TRENDS");
83 | });
84 |
85 | it("should handle errors gracefully", async () => {
86 | // Test with invalid parameters
87 | const result = await checkKGHealth({
88 | includeHistory: true,
89 | generateReport: true,
90 | days: -1, // Invalid
91 | });
92 |
93 | // Should either handle gracefully or return error
94 | expect(result.content).toBeDefined();
95 | });
96 |
97 | it("should calculate health score", async () => {
98 | const result = await checkKGHealth({
99 | includeHistory: false,
100 | generateReport: true,
101 | });
102 |
103 | expect(result.content).toBeDefined();
104 | const text = result.content.map((c) => c.text).join(" ");
105 |
106 | // Should contain some health indicator
107 | expect(text.length).toBeGreaterThan(0);
108 | });
109 |
110 | it("should include critical issues in next steps", async () => {
111 | // Create a project with some data to trigger health calculation
112 | const kg = await getKnowledgeGraph();
113 |
114 | // Add some nodes and edges to test health calculation
115 | kg.addNode({
116 | id: "test-node-1",
117 | type: "project",
118 | label: "Test Project",
119 | properties: { name: "test" },
120 | weight: 1.0,
121 | });
122 |
123 | const result = await checkKGHealth({
124 | includeHistory: false,
125 | generateReport: true,
126 | });
127 |
128 | expect(result.content).toBeDefined();
129 |
130 | // Check that the response structure is correct
131 | const text = result.content.map((c) => c.text).join(" ");
132 | expect(text).toBeTruthy();
133 | });
134 |
135 | it("should handle graph with high data quality score", async () => {
136 | const result = await checkKGHealth({
137 | includeHistory: false,
138 | generateReport: true,
139 | });
140 |
141 | expect(result.content).toBeDefined();
142 | const text = result.content.map((c) => c.text).join(" ");
143 |
144 | // Should complete without errors
145 | expect(text.length).toBeGreaterThan(0);
146 | });
147 |
148 | it("should use default values when parameters not provided", async () => {
149 | const result = await checkKGHealth({});
150 |
151 | expect(result.content).toBeDefined();
152 | expect(result.content.length).toBeGreaterThan(0);
153 | });
154 |
155 | it("should handle various health score ranges in report", async () => {
156 | // Test the helper functions indirectly through the report
157 | const result = await checkKGHealth({
158 | includeHistory: false,
159 | generateReport: true,
160 | });
161 |
162 | expect(result.content).toBeDefined();
163 | const text = result.content.map((c) => c.text).join(" ");
164 |
165 | // Should contain health indicators (emojis or text)
166 | expect(text.length).toBeGreaterThan(0);
167 | });
168 |
169 | it("should handle different trend directions in report", async () => {
170 | const result = await checkKGHealth({
171 | includeHistory: true,
172 | generateReport: true,
173 | });
174 |
175 | expect(result.content).toBeDefined();
176 | const text = result.content.map((c) => c.text).join(" ");
177 |
178 | // Report should include trend information
179 | expect(text).toContain("TRENDS");
180 | });
181 |
182 | it("should handle different priority levels in recommendations", async () => {
183 | const result = await checkKGHealth({
184 | includeHistory: false,
185 | generateReport: true,
186 | });
187 |
188 | expect(result.content).toBeDefined();
189 | const text = result.content.map((c) => c.text).join(" ");
190 |
191 | // Should complete without errors
192 | expect(text.length).toBeGreaterThan(0);
193 | });
194 |
195 | it("should handle different byte sizes in formatBytes", async () => {
196 | const result = await checkKGHealth({
197 | includeHistory: false,
198 | generateReport: true,
199 | });
200 |
201 | expect(result.content).toBeDefined();
202 | const text = result.content.map((c) => c.text).join(" ");
203 |
204 | // Report should include storage size
205 | expect(text.length).toBeGreaterThan(0);
206 | });
207 |
208 | it("should handle validation errors", async () => {
209 | const result = await checkKGHealth({
210 | days: 150, // Exceeds max of 90
211 | });
212 |
213 | expect(result.content).toBeDefined();
214 | // Should return error response
215 | const text = result.content.map((c) => c.text).join(" ");
216 | expect(text).toBeTruthy();
217 | });
218 |
219 | it("should handle recommendations with different priorities", async () => {
220 | const result = await checkKGHealth({
221 | includeHistory: false,
222 | generateReport: true,
223 | });
224 |
225 | expect(result.content).toBeDefined();
226 |
227 | // Check response structure
228 | const text = result.content.map((c) => c.text).join(" ");
229 | expect(text.length).toBeGreaterThan(0);
230 | });
231 |
232 | it("should detect and report data quality issues", async () => {
233 | const kg = await getKnowledgeGraph();
234 |
235 | // Create nodes
236 | kg.addNode({
237 | id: "test-project-1",
238 | type: "project",
239 | label: "Test Project 1",
240 | properties: { name: "test-project-1" },
241 | weight: 1.0,
242 | });
243 |
244 | kg.addNode({
245 | id: "test-tech-1",
246 | type: "technology",
247 | label: "TypeScript",
248 | properties: { name: "typescript" },
249 | weight: 1.0,
250 | });
251 |
252 | // Create an orphaned edge (edge pointing to non-existent node)
253 | kg.addEdge({
254 | source: "test-tech-1",
255 | target: "non-existent-node-id",
256 | type: "uses",
257 | weight: 1.0,
258 | confidence: 0.9,
259 | properties: {},
260 | });
261 |
262 | const result = await checkKGHealth({
263 | includeHistory: false,
264 | generateReport: true,
265 | });
266 |
267 | expect(result.content).toBeDefined();
268 | const text = result.content.map((c) => c.text).join(" ");
269 |
270 | // Should report data quality issues with score < 90
271 | expect(text).toContain("Health");
272 | // The report should show details about stale nodes, orphaned edges, etc.
273 | expect(text.length).toBeGreaterThan(100); // Detailed report
274 | });
275 |
276 | it("should test all priority icon levels", async () => {
277 | // This test indirectly tests getPriorityIcon for "high", "medium", and "low"
278 | const result = await checkKGHealth({
279 | includeHistory: false,
280 | generateReport: true,
281 | });
282 |
283 | expect(result.content).toBeDefined();
284 | const text = result.content.map((c) => c.text).join(" ");
285 |
286 | // The report should include priority indicators (emojis)
287 | expect(text.length).toBeGreaterThan(0);
288 | });
289 |
290 | it("should test formatBytes for different size ranges", async () => {
291 | // The tool will calculate storage size which triggers formatBytes
292 | // This covers: bytes, KB, MB ranges
293 | const result = await checkKGHealth({
294 | includeHistory: false,
295 | generateReport: true,
296 | });
297 |
298 | expect(result.content).toBeDefined();
299 | const text = result.content.map((c) => c.text).join(" ");
300 |
301 | // Storage size should be included in the report
302 | expect(text.length).toBeGreaterThan(0);
303 | });
304 | });
305 |
```
--------------------------------------------------------------------------------
/src/utils/usage-metadata.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | DriftSnapshot,
3 | UsageMetadata,
4 | DocumentationSection,
5 | } from "./drift-detector.js";
6 | import { ASTAnalyzer, CallGraph } from "./ast-analyzer.js";
7 |
8 | /**
9 | * Builds UsageMetadata from snapshot artifacts so priority scoring can
10 | * incorporate real import/reference counts and call graph analysis.
11 | *
12 | * Enhanced implementation (ADR-012 Phase 2):
13 | * - Builds call graphs for exported functions to count actual usage
14 | * - Extracts class instantiations from AST analysis
15 | * - Counts imports with better accuracy
16 | * - Incorporates documentation references
17 | */
18 | export class UsageMetadataCollector {
19 | private analyzer: ASTAnalyzer;
20 |
21 | constructor(analyzer?: ASTAnalyzer) {
22 | this.analyzer = analyzer || new ASTAnalyzer();
23 | }
24 |
25 | /**
26 | * Collect usage metadata from snapshot using AST call graph analysis
27 | */
28 | async collect(snapshot: DriftSnapshot): Promise<UsageMetadata> {
29 | const functionCalls = new Map<string, number>();
30 | const classInstantiations = new Map<string, number>();
31 | const imports = new Map<string, number>();
32 | const exportedSymbols = new Set<string>();
33 | const functionSymbols = new Map<string, { file: string; signature: any }>();
34 | const classSymbols = new Map<string, { file: string; info: any }>();
35 |
36 | // Initialize analyzer if needed
37 | await this.analyzer.initialize();
38 |
39 | // Phase 1: Collect all exported symbols and their locations
40 | for (const [filePath, fileAnalysis] of snapshot.files.entries()) {
41 | // Track exports
42 | for (const symbol of fileAnalysis.exports ?? []) {
43 | exportedSymbols.add(symbol);
44 | }
45 |
46 | // Track functions with their file locations
47 | for (const fn of fileAnalysis.functions ?? []) {
48 | if (fn.name) {
49 | functionSymbols.set(fn.name, { file: filePath, signature: fn });
50 | }
51 | }
52 |
53 | // Track classes with their file locations
54 | for (const cls of fileAnalysis.classes ?? []) {
55 | if (cls.name) {
56 | classSymbols.set(cls.name, { file: filePath, info: cls });
57 | }
58 | }
59 | }
60 |
61 | // Phase 2: Build call graphs for exported functions to count actual usage
62 | const callGraphPromises: Promise<CallGraph | null>[] = [];
63 | const exportedFunctions = Array.from(exportedSymbols).filter((name) =>
64 | functionSymbols.has(name),
65 | );
66 |
67 | for (const funcName of exportedFunctions.slice(0, 50)) {
68 | // Limit to top 50 exported functions to avoid performance issues
69 | const funcInfo = functionSymbols.get(funcName);
70 | if (funcInfo) {
71 | callGraphPromises.push(
72 | this.analyzer
73 | .buildCallGraph(funcName, funcInfo.file, {
74 | maxDepth: 2, // Limit depth for performance
75 | resolveImports: true,
76 | extractConditionals: false, // Skip for performance
77 | trackExceptions: false,
78 | })
79 | .catch(() => null), // Gracefully handle failures
80 | );
81 | }
82 | }
83 |
84 | const callGraphs = await Promise.all(callGraphPromises);
85 |
86 | // Count function calls from call graphs
87 | for (const graph of callGraphs) {
88 | if (!graph) continue;
89 |
90 | // Count calls recursively in the call graph
91 | const countCalls = (node: any): void => {
92 | if (!node.function?.name) return;
93 |
94 | const funcName = node.function.name;
95 | functionCalls.set(funcName, (functionCalls.get(funcName) ?? 0) + 1);
96 |
97 | // Recursively count child calls
98 | for (const childCall of node.calls ?? []) {
99 | countCalls(childCall);
100 | }
101 | };
102 |
103 | countCalls(graph.root);
104 | }
105 |
106 | // Phase 3: Count imports and infer usage
107 | for (const fileAnalysis of snapshot.files.values()) {
108 | for (const imp of fileAnalysis.imports ?? []) {
109 | for (const imported of imp.imports ?? []) {
110 | const name = imported.alias || imported.name;
111 | if (name) {
112 | imports.set(name, (imports.get(name) ?? 0) + 1);
113 |
114 | // If imported symbol is an exported function/class, count as usage
115 | const isFunction = functionSymbols.has(name);
116 | const isClass = classSymbols.has(name);
117 |
118 | if (exportedSymbols.has(name)) {
119 | if (isClass) {
120 | classInstantiations.set(
121 | name,
122 | (classInstantiations.get(name) ?? 0) + 1,
123 | );
124 | } else if (isFunction) {
125 | // Only count if not already counted from call graph
126 | if (!functionCalls.has(name)) {
127 | functionCalls.set(name, (functionCalls.get(name) ?? 0) + 1);
128 | }
129 | }
130 | }
131 | }
132 | }
133 | }
134 | }
135 |
136 | // Phase 4: Extract class instantiations from AST
137 | // Look for "new ClassName()" patterns in function bodies
138 | for (const fileAnalysis of snapshot.files.values()) {
139 | for (const fn of fileAnalysis.functions ?? []) {
140 | // Check function dependencies for class instantiations
141 | for (const dep of fn.dependencies ?? []) {
142 | if (classSymbols.has(dep)) {
143 | classInstantiations.set(
144 | dep,
145 | (classInstantiations.get(dep) ?? 0) + 1,
146 | );
147 | }
148 | }
149 | }
150 | }
151 |
152 | // Phase 5: Count references from documentation sections
153 | const bumpRefs = (
154 | sections: DocumentationSection[],
155 | target: "fn" | "cls",
156 | ) => {
157 | for (const section of sections) {
158 | const refs =
159 | target === "fn"
160 | ? section.referencedFunctions ?? []
161 | : section.referencedClasses ?? [];
162 | for (const ref of refs) {
163 | if (target === "fn") {
164 | functionCalls.set(ref, (functionCalls.get(ref) ?? 0) + 1);
165 | } else {
166 | classInstantiations.set(
167 | ref,
168 | (classInstantiations.get(ref) ?? 0) + 1,
169 | );
170 | }
171 | }
172 | }
173 | };
174 |
175 | for (const doc of snapshot.documentation.values()) {
176 | bumpRefs(doc.sections ?? [], "fn");
177 | bumpRefs(doc.sections ?? [], "cls");
178 | }
179 |
180 | return {
181 | filePath: snapshot.projectPath,
182 | functionCalls,
183 | classInstantiations,
184 | imports,
185 | };
186 | }
187 |
188 | /**
189 | * Synchronous collection using heuristics (fallback when analyzer unavailable)
190 | * This maintains backward compatibility with existing code
191 | */
192 | collectSync(snapshot: DriftSnapshot): UsageMetadata {
193 | const functionCalls = new Map<string, number>();
194 | const classInstantiations = new Map<string, number>();
195 | const imports = new Map<string, number>();
196 | const exportedSymbols = new Set<string>();
197 | const functionSymbols = new Set<string>();
198 | const classSymbols = new Set<string>();
199 |
200 | // Collect exported symbols and discovered functions/classes
201 | for (const file of snapshot.files.values()) {
202 | for (const symbol of file.exports ?? []) {
203 | exportedSymbols.add(symbol);
204 | }
205 | for (const fn of file.functions ?? []) {
206 | if (fn.name) functionSymbols.add(fn.name);
207 | }
208 | for (const cls of file.classes ?? []) {
209 | if (cls.name) classSymbols.add(cls.name);
210 | }
211 | }
212 |
213 | // Count imports from source files
214 | for (const file of snapshot.files.values()) {
215 | for (const imp of file.imports ?? []) {
216 | for (const imported of imp.imports ?? []) {
217 | const name = imported.alias || imported.name;
218 | if (name) {
219 | imports.set(name, (imports.get(name) ?? 0) + 1);
220 |
221 | const isFunction = functionSymbols.has(name);
222 | const isClass = classSymbols.has(name);
223 | if (exportedSymbols.has(name) || isFunction || isClass) {
224 | if (isClass) {
225 | classInstantiations.set(
226 | name,
227 | (classInstantiations.get(name) ?? 0) + 1,
228 | );
229 | } else {
230 | functionCalls.set(name, (functionCalls.get(name) ?? 0) + 1);
231 | }
232 | }
233 | }
234 | }
235 | }
236 | }
237 |
238 | // Count references from documentation sections
239 | const bumpRefs = (
240 | sections: DocumentationSection[],
241 | target: "fn" | "cls",
242 | ) => {
243 | for (const section of sections) {
244 | const refs =
245 | target === "fn"
246 | ? section.referencedFunctions ?? []
247 | : section.referencedClasses ?? [];
248 | for (const ref of refs) {
249 | if (target === "fn") {
250 | functionCalls.set(ref, (functionCalls.get(ref) ?? 0) + 1);
251 | } else {
252 | classInstantiations.set(
253 | ref,
254 | (classInstantiations.get(ref) ?? 0) + 1,
255 | );
256 | }
257 | }
258 | }
259 | };
260 |
261 | for (const doc of snapshot.documentation.values()) {
262 | bumpRefs(doc.sections ?? [], "fn");
263 | bumpRefs(doc.sections ?? [], "cls");
264 | }
265 |
266 | return {
267 | filePath: snapshot.projectPath,
268 | functionCalls,
269 | classInstantiations,
270 | imports,
271 | };
272 | }
273 | }
274 |
```
--------------------------------------------------------------------------------
/docs/development/MCP_INSPECTOR_TESTING.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | documcp:
3 | last_updated: "2025-11-20T00:46:21.945Z"
4 | last_validated: "2025-12-09T19:41:38.576Z"
5 | auto_updated: false
6 | update_frequency: monthly
7 | validated_against_commit: 306567b32114502c606244ad6c2930360bcd4201
8 | ---
9 |
10 | # MCP Inspector Testing Guide
11 |
12 | The MCP Inspector is an in-browser debugging tool for testing MCP servers without connecting to actual applications. This guide explains how to use it for DocuMCP development.
13 |
14 | ## Prerequisites
15 |
16 | - Node.js 20+ installed
17 | - DocuMCP repository cloned
18 | - Dependencies installed (`npm install`)
19 |
20 | ## Quick Start
21 |
22 | ### Option 1: Build and Launch Inspector
23 |
24 | ```bash
25 | npm run build:inspect
26 | ```
27 |
28 | This command:
29 |
30 | 1. Compiles TypeScript to `dist/`
31 | 2. Launches MCP Inspector
32 | 3. Opens browser at `http://localhost:5173` (or similar)
33 |
34 | ### Option 2: Launch Inspector with Existing Build
35 |
36 | ```bash
37 | npm run build # First build (if needed)
38 | npm run dev:inspect # Then launch inspector
39 | ```
40 |
41 | ## Using the Inspector
42 |
43 | ### 1. Connect to Server
44 |
45 | 1. Open the browser URL provided by the inspector
46 | 2. Click the "Connect" button in the left sidebar
47 | 3. Wait for connection confirmation
48 |
49 | ### 2. Test Tools
50 |
51 | The Tools section lists all available MCP tools:
52 |
53 | **Example: Testing `analyze_repository`**
54 |
55 | 1. Click "Tools" in the top navigation
56 | 2. Select "analyze_repository" from the list
57 | 3. In the right panel, enter parameters:
58 | ```json
59 | {
60 | "path": "./",
61 | "depth": "standard"
62 | }
63 | ```
64 | 4. Click "Run Tool"
65 | 5. Verify the output includes:
66 | - File counts
67 | - Language detection
68 | - Dependency analysis
69 | - Memory insights
70 |
71 | **Example: Testing `recommend_ssg`**
72 |
73 | 1. First run `analyze_repository` (as above) to get an `analysisId`
74 | 2. Select "recommend_ssg"
75 | 3. Enter parameters:
76 | ```json
77 | {
78 | "analysisId": "<id-from-previous-analysis>",
79 | "userId": "test-user",
80 | "preferences": {
81 | "priority": "simplicity",
82 | "ecosystem": "javascript"
83 | }
84 | }
85 | ```
86 | 4. Click "Run Tool"
87 | 5. Verify recommendation includes:
88 | - Recommended SSG
89 | - Confidence score
90 | - Reasoning
91 | - Alternative options
92 |
93 | ### 3. Test Resources
94 |
95 | Resources provide static data for application UIs:
96 |
97 | **Example: Testing SSG List**
98 |
99 | 1. Click "Resources" in the top navigation
100 | 2. Select "documcp://ssgs/available"
101 | 3. Verify output shows all 5 SSGs:
102 | - Jekyll
103 | - Hugo
104 | - Docusaurus
105 | - MkDocs
106 | - Eleventy
107 | 4. Check each SSG includes:
108 | - ID, name, description
109 | - Language, complexity, build speed
110 | - Best use cases
111 |
112 | **Example: Testing Configuration Templates**
113 |
114 | 1. Select "documcp://templates/jekyll-config"
115 | 2. Verify YAML template is returned
116 | 3. Test other templates:
117 | - `documcp://templates/hugo-config`
118 | - `documcp://templates/docusaurus-config`
119 | - `documcp://templates/mkdocs-config`
120 | - `documcp://templates/eleventy-config`
121 | - `documcp://templates/diataxis-structure`
122 |
123 | ### 4. Test Prompts
124 |
125 | Prompts provide pre-written instructions for specialized tasks:
126 |
127 | **Example: Testing `tutorial-writer`**
128 |
129 | 1. Click "Prompts" in the top navigation
130 | 2. Select "tutorial-writer"
131 | 3. Provide arguments:
132 | ```json
133 | {
134 | "project_path": "./",
135 | "target_audience": "beginners",
136 | "learning_goal": "deploy first documentation site"
137 | }
138 | ```
139 | 4. Click "Get Prompt"
140 | 5. Verify prompt messages include:
141 | - Project context (languages, frameworks)
142 | - Diataxis tutorial requirements
143 | - Step-by-step structure guidance
144 |
145 | **Example: Testing `analyze-and-recommend` workflow**
146 |
147 | 1. Select "analyze-and-recommend"
148 | 2. Provide arguments:
149 | ```json
150 | {
151 | "project_path": "./",
152 | "analysis_depth": "standard",
153 | "preferences": "good community support"
154 | }
155 | ```
156 | 3. Verify workflow prompt includes:
157 | - Complete analysis workflow
158 | - SSG recommendation guidance
159 | - Implementation steps
160 |
161 | ## Common Test Cases
162 |
163 | ### Tool Testing Checklist
164 |
165 | - [ ] **analyze_repository**
166 |
167 | - [ ] Test with current directory (`./`)
168 | - [ ] Test with different depth levels
169 | - [ ] Verify memory integration works
170 | - [ ] Check similar projects are found
171 |
172 | - [ ] **recommend_ssg**
173 |
174 | - [ ] Test with valid analysisId
175 | - [ ] Test different preference combinations
176 | - [ ] Verify confidence scores
177 | - [ ] Check historical data integration
178 |
179 | - [ ] **generate_config**
180 |
181 | - [ ] Test each SSG type
182 | - [ ] Verify output format
183 | - [ ] Check template variables
184 |
185 | - [ ] **setup_structure**
186 |
187 | - [ ] Test Diataxis structure creation
188 | - [ ] Verify all categories included
189 | - [ ] Check example content
190 |
191 | - [ ] **deploy_pages**
192 |
193 | - [ ] Test workflow generation
194 | - [ ] Verify GitHub Actions YAML
195 | - [ ] Check custom domain support
196 |
197 | - [ ] **validate_content**
198 | - [ ] Test with documentation path
199 | - [ ] Verify link checking
200 | - [ ] Check code block validation
201 |
202 | ### Resource Testing Checklist
203 |
204 | - [ ] **documcp://ssgs/available**
205 |
206 | - [ ] All 5 SSGs listed
207 | - [ ] Complete metadata for each
208 |
209 | - [ ] **Templates**
210 |
211 | - [ ] Jekyll config valid YAML
212 | - [ ] Hugo config valid YAML
213 | - [ ] Docusaurus config valid JS
214 | - [ ] MkDocs config valid YAML
215 | - [ ] Eleventy config valid JS
216 | - [ ] Diataxis structure valid JSON
217 |
218 | - [ ] **Workflows**
219 | - [ ] All workflows listed
220 | - [ ] Quick setup available
221 | - [ ] Full setup available
222 | - [ ] Guidance provided
223 |
224 | ### Prompt Testing Checklist
225 |
226 | - [ ] **Technical Writer Prompts**
227 |
228 | - [ ] tutorial-writer
229 | - [ ] howto-guide-writer
230 | - [ ] reference-writer
231 | - [ ] explanation-writer
232 | - [ ] diataxis-organizer
233 | - [ ] readme-optimizer
234 |
235 | - [ ] **Workflow Prompts**
236 | - [ ] analyze-and-recommend
237 | - [ ] setup-documentation
238 | - [ ] troubleshoot-deployment
239 |
240 | ## Troubleshooting
241 |
242 | ### Inspector Won't Connect
243 |
244 | **Problem:** Connection fails or times out
245 |
246 | **Solutions:**
247 |
248 | 1. Ensure server is built: `npm run build`
249 | 2. Check no other process is using the port
250 | 3. Try restarting: `Ctrl+C` and re-run `npm run dev:inspect`
251 |
252 | ### Tool Returns Error
253 |
254 | **Problem:** Tool execution fails with error message
255 |
256 | **Solutions:**
257 |
258 | 1. Check parameter format (must be valid JSON)
259 | 2. Verify required parameters are provided
260 | 3. Ensure file paths exist (for file-based tools)
261 | 4. Check server logs for detailed error messages
262 |
263 | ### Resource Not Found
264 |
265 | **Problem:** Resource URI returns "Resource not found" error
266 |
267 | **Solutions:**
268 |
269 | 1. Verify URI spelling matches exactly (case-sensitive)
270 | 2. Check resource list for available URIs
271 | 3. Ensure server version matches documentation
272 |
273 | ### Prompt Arguments Missing
274 |
275 | **Problem:** Prompt doesn't use provided arguments
276 |
277 | **Solutions:**
278 |
279 | 1. Check argument names match prompt definition
280 | 2. Verify JSON format is correct
281 | 3. Required arguments must be provided
282 |
283 | ## Best Practices
284 |
285 | ### During Development
286 |
287 | 1. **Keep Inspector Open:** Launch inspector at start of development session
288 | 2. **Test After Changes:** Run tool tests after modifying tool implementation
289 | 3. **Verify All Paths:** Test both success and error paths
290 | 4. **Check Edge Cases:** Test with unusual inputs, empty values, etc.
291 |
292 | ### Before Committing
293 |
294 | 1. **Full Tool Test:** Test at least one example from each tool
295 | 2. **Resource Validation:** Verify all resources return valid data
296 | 3. **Prompt Verification:** Check prompts generate correct messages
297 | 4. **Error Handling:** Test with invalid inputs to verify error messages
298 |
299 | ### For Bug Fixing
300 |
301 | 1. **Reproduce in Inspector:** Use inspector to reproduce bug consistently
302 | 2. **Test Fix:** Verify fix works in inspector before integration testing
303 | 3. **Regression Test:** Test related tools to ensure no regressions
304 | 4. **Document:** Add test case to this guide if bug was subtle
305 |
306 | ## Integration with Development Workflow
307 |
308 | ### Daily Development
309 |
310 | ```bash
311 | # Morning startup
312 | npm run build:inspect
313 |
314 | # Keep inspector tab open
315 | # Make code changes in editor
316 | # Test changes in inspector
317 | # Iterate until working
318 |
319 | # Before lunch/end of day
320 | npm run build && npm test
321 | ```
322 |
323 | ### Pre-Commit Workflow
324 |
325 | ```bash
326 | # Run full validation
327 | npm run ci
328 |
329 | # Test in inspector
330 | npm run build:inspect
331 |
332 | # Manual spot checks on key tools
333 | # Commit when all checks pass
334 | ```
335 |
336 | ### CI/CD Integration
337 |
338 | While MCP Inspector is primarily for local development, you can add automated checks:
339 |
340 | ```bash
341 | # In CI pipeline (future enhancement)
342 | npm run build
343 | npx @modelcontextprotocol/inspector dist/index.js --test automated-tests.json
344 | ```
345 |
346 | ## Additional Resources
347 |
348 | - **MCP Inspector GitHub:** https://github.com/modelcontextprotocol/inspector
349 | - **MCP Specification:** https://modelcontextprotocol.io/docs
350 | - **MCP TypeScript SDK:** https://github.com/modelcontextprotocol/typescript-sdk
351 | - **DocuMCP Architecture:** See `docs/adrs/` for detailed architectural decisions
352 |
353 | ## Feedback
354 |
355 | If you encounter issues with MCP Inspector or this guide:
356 |
357 | 1. Check for known issues: https://github.com/modelcontextprotocol/inspector/issues
358 | 2. Report DocuMCP-specific issues: https://github.com/anthropics/documcp/issues
359 | 3. Suggest improvements to this guide via pull request
360 |
361 | ---
362 |
363 | **Last Updated:** 2025-10-09
364 | **Version:** 1.0.0
365 |
```
--------------------------------------------------------------------------------
/tests/prompts/technical-writer-prompts.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | generateTechnicalWriterPrompts,
3 | analyzeProjectContext,
4 | } from "../../src/prompts/technical-writer-prompts.js";
5 | import { promises as fs } from "fs";
6 | import { join } from "path";
7 | import { tmpdir } from "os";
8 |
9 | describe("Technical Writer Diataxis Prompts", () => {
10 | let testProjectPath: string;
11 |
12 | beforeEach(async () => {
13 | // Create a temporary test project
14 | testProjectPath = join(tmpdir(), `test-project-${Date.now()}`);
15 | await fs.mkdir(testProjectPath, { recursive: true });
16 |
17 | // Create a basic package.json
18 | const packageJson = {
19 | name: "test-project",
20 | version: "1.0.0",
21 | dependencies: {
22 | react: "^18.0.0",
23 | typescript: "^5.0.0",
24 | },
25 | scripts: {
26 | test: "jest",
27 | },
28 | };
29 | await fs.writeFile(
30 | join(testProjectPath, "package.json"),
31 | JSON.stringify(packageJson, null, 2),
32 | );
33 |
34 | // Create a basic README.md
35 | await fs.writeFile(
36 | join(testProjectPath, "README.md"),
37 | "# Test Project\n\nA test project for testing.",
38 | );
39 |
40 | // Create a test directory
41 | await fs.mkdir(join(testProjectPath, "tests"), { recursive: true });
42 | await fs.writeFile(
43 | join(testProjectPath, "tests", "example.test.js"),
44 | 'test("example", () => { expect(true).toBe(true); });',
45 | );
46 |
47 | // Create a CI file
48 | await fs.mkdir(join(testProjectPath, ".github", "workflows"), {
49 | recursive: true,
50 | });
51 | await fs.writeFile(
52 | join(testProjectPath, ".github", "workflows", "ci.yml"),
53 | "name: CI\non: [push, pull_request]",
54 | );
55 | });
56 |
57 | afterEach(async () => {
58 | // Clean up test project
59 | try {
60 | await fs.rm(testProjectPath, { recursive: true, force: true });
61 | } catch (error) {
62 | // Ignore cleanup errors
63 | }
64 | });
65 |
66 | describe("generateTechnicalWriterPrompts", () => {
67 | it("should generate tutorial writer prompts", async () => {
68 | const prompts = await generateTechnicalWriterPrompts(
69 | "tutorial-writer",
70 | testProjectPath,
71 | );
72 |
73 | expect(prompts.length).toBeGreaterThan(0);
74 | expect(prompts[0]).toHaveProperty("role");
75 | expect(prompts[0]).toHaveProperty("content");
76 | expect(prompts[0].content).toHaveProperty("type", "text");
77 | expect(prompts[0].content).toHaveProperty("text");
78 | expect(prompts[0].content.text).toContain("tutorial");
79 | expect(prompts[0].content.text).toContain("Diataxis");
80 | });
81 |
82 | it("should generate how-to guide writer prompts", async () => {
83 | const prompts = await generateTechnicalWriterPrompts(
84 | "howto-guide-writer",
85 | testProjectPath,
86 | );
87 |
88 | expect(prompts.length).toBeGreaterThan(0);
89 | expect(prompts[0].content.text).toContain("how-to guide");
90 | expect(prompts[0].content.text).toContain("Problem-oriented");
91 | });
92 |
93 | it("should generate reference writer prompts", async () => {
94 | const prompts = await generateTechnicalWriterPrompts(
95 | "reference-writer",
96 | testProjectPath,
97 | );
98 |
99 | expect(prompts.length).toBeGreaterThan(0);
100 | expect(prompts[0].content.text).toContain("reference documentation");
101 | expect(prompts[0].content.text).toContain("Information-oriented");
102 | });
103 |
104 | it("should generate explanation writer prompts", async () => {
105 | const prompts = await generateTechnicalWriterPrompts(
106 | "explanation-writer",
107 | testProjectPath,
108 | );
109 |
110 | expect(prompts.length).toBeGreaterThan(0);
111 | expect(prompts[0].content.text).toContain("explanation documentation");
112 | expect(prompts[0].content.text).toContain("Understanding-oriented");
113 | });
114 |
115 | it("should generate diataxis organizer prompts", async () => {
116 | const prompts = await generateTechnicalWriterPrompts(
117 | "diataxis-organizer",
118 | testProjectPath,
119 | );
120 |
121 | expect(prompts.length).toBeGreaterThan(0);
122 | expect(prompts[0].content.text).toContain("Diataxis framework");
123 | expect(prompts[0].content.text).toContain("organize");
124 | });
125 |
126 | it("should generate readme optimizer prompts", async () => {
127 | const prompts = await generateTechnicalWriterPrompts(
128 | "readme-optimizer",
129 | testProjectPath,
130 | );
131 |
132 | expect(prompts.length).toBeGreaterThan(0);
133 | expect(prompts[0].content.text).toContain("README");
134 | expect(prompts[0].content.text).toContain("Diataxis-aware");
135 | });
136 |
137 | it("should generate analyze-and-recommend prompts", async () => {
138 | const prompts = await generateTechnicalWriterPrompts(
139 | "analyze-and-recommend",
140 | testProjectPath,
141 | );
142 |
143 | expect(prompts.length).toBeGreaterThan(0);
144 | expect(prompts[0].content.text.toLowerCase()).toContain("analyz");
145 | expect(prompts[0].content.text.toLowerCase()).toContain("recommend");
146 | });
147 |
148 | it("should generate setup-documentation prompts", async () => {
149 | const prompts = await generateTechnicalWriterPrompts(
150 | "setup-documentation",
151 | testProjectPath,
152 | );
153 |
154 | expect(prompts.length).toBeGreaterThan(0);
155 | expect(prompts[0].content.text).toContain("documentation");
156 | });
157 |
158 | it("should generate troubleshoot-deployment prompts", async () => {
159 | const prompts = await generateTechnicalWriterPrompts(
160 | "troubleshoot-deployment",
161 | testProjectPath,
162 | );
163 |
164 | expect(prompts.length).toBeGreaterThan(0);
165 | expect(prompts[0].content.text).toContain("troubleshoot");
166 | expect(prompts[0].content.text).toContain("deployment");
167 | });
168 |
169 | it("should generate maintain-documentation-freshness prompts", async () => {
170 | const prompts = await generateTechnicalWriterPrompts(
171 | "maintain-documentation-freshness",
172 | testProjectPath,
173 | { action: "track", preset: "monthly" },
174 | );
175 |
176 | expect(prompts.length).toBeGreaterThan(0);
177 | expect(prompts[0].content.text).toContain("freshness");
178 | expect(prompts[0].content.text).toContain("track");
179 | });
180 |
181 | it("should throw error for unknown prompt type", async () => {
182 | await expect(
183 | generateTechnicalWriterPrompts("unknown-type", testProjectPath),
184 | ).rejects.toThrow("Unknown prompt type: unknown-type");
185 | });
186 |
187 | it("should include project context in prompts", async () => {
188 | const prompts = await generateTechnicalWriterPrompts(
189 | "tutorial-writer",
190 | testProjectPath,
191 | );
192 |
193 | const promptText = prompts[0].content.text;
194 | expect(promptText).toContain("React"); // Should detect React from package.json
195 | expect(promptText).toContain("TypeScript"); // Should detect TypeScript
196 | });
197 | });
198 |
199 | describe("analyzeProjectContext", () => {
200 | it("should analyze project context correctly", async () => {
201 | const context = await analyzeProjectContext(testProjectPath);
202 |
203 | expect(context).toHaveProperty("projectType");
204 | expect(context).toHaveProperty("languages");
205 | expect(context).toHaveProperty("frameworks");
206 | expect(context).toHaveProperty("hasTests");
207 | expect(context).toHaveProperty("hasCI");
208 | expect(context).toHaveProperty("readmeExists");
209 | expect(context).toHaveProperty("documentationGaps");
210 |
211 | // Check specific values based on our test setup
212 | expect(context.projectType).toBe("node_application");
213 | expect(context.languages).toContain("TypeScript");
214 | expect(context.frameworks).toContain("React");
215 | expect(context.hasTests).toBe(true);
216 | expect(context.hasCI).toBe(true);
217 | expect(context.readmeExists).toBe(true);
218 | expect(context.packageManager).toBe("npm");
219 | });
220 |
221 | it("should detect documentation gaps", async () => {
222 | const context = await analyzeProjectContext(testProjectPath);
223 |
224 | expect(Array.isArray(context.documentationGaps)).toBe(true);
225 | // Should detect missing documentation since we only have a basic README
226 | expect(context.documentationGaps.length).toBeGreaterThan(0);
227 | });
228 |
229 | it("should handle projects without package.json", async () => {
230 | // Create a project without package.json
231 | const simpleProjectPath = join(tmpdir(), `simple-project-${Date.now()}`);
232 | await fs.mkdir(simpleProjectPath, { recursive: true });
233 |
234 | try {
235 | const context = await analyzeProjectContext(simpleProjectPath);
236 |
237 | expect(context.projectType).toBe("unknown");
238 | expect(context.languages).toEqual([]);
239 | expect(context.frameworks).toEqual([]);
240 | expect(context.readmeExists).toBe(false);
241 | } finally {
242 | await fs.rm(simpleProjectPath, { recursive: true, force: true });
243 | }
244 | });
245 |
246 | it("should detect yarn package manager", async () => {
247 | // Create yarn.lock to simulate yarn project
248 | await fs.writeFile(join(testProjectPath, "yarn.lock"), "# Yarn lockfile");
249 |
250 | const context = await analyzeProjectContext(testProjectPath);
251 | expect(context.packageManager).toBe("yarn");
252 | });
253 |
254 | it("should detect pnpm package manager", async () => {
255 | // Create pnpm-lock.yaml to simulate pnpm project
256 | await fs.writeFile(
257 | join(testProjectPath, "pnpm-lock.yaml"),
258 | "lockfileVersion: 5.4",
259 | );
260 |
261 | const context = await analyzeProjectContext(testProjectPath);
262 | expect(context.packageManager).toBe("pnpm");
263 | });
264 | });
265 | });
266 |
```
--------------------------------------------------------------------------------
/src/utils/freshness-tracker.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Documentation Freshness Tracking Utilities
3 | *
4 | * Tracks when documentation files were last updated and validated,
5 | * supporting both short-term (minutes/hours) and long-term (days) staleness detection.
6 | */
7 |
8 | import fs from "fs/promises";
9 | import path from "path";
10 | import matter from "gray-matter";
11 |
12 | /**
13 | * Time unit for staleness threshold
14 | */
15 | export type TimeUnit = "minutes" | "hours" | "days";
16 |
17 | /**
18 | * Staleness threshold configuration
19 | */
20 | export interface StalenessThreshold {
21 | value: number;
22 | unit: TimeUnit;
23 | }
24 |
25 | /**
26 | * Predefined staleness levels
27 | */
28 | export const STALENESS_PRESETS = {
29 | realtime: { value: 30, unit: "minutes" as TimeUnit },
30 | active: { value: 1, unit: "hours" as TimeUnit },
31 | recent: { value: 24, unit: "hours" as TimeUnit },
32 | weekly: { value: 7, unit: "days" as TimeUnit },
33 | monthly: { value: 30, unit: "days" as TimeUnit },
34 | quarterly: { value: 90, unit: "days" as TimeUnit },
35 | } as const;
36 |
37 | /**
38 | * Documentation metadata tracked in frontmatter
39 | */
40 | export interface DocFreshnessMetadata {
41 | last_updated?: string; // ISO 8601 timestamp
42 | last_validated?: string; // ISO 8601 timestamp
43 | validated_against_commit?: string;
44 | auto_updated?: boolean;
45 | staleness_threshold?: StalenessThreshold;
46 | update_frequency?: keyof typeof STALENESS_PRESETS;
47 | }
48 |
49 | /**
50 | * Full frontmatter structure
51 | */
52 | export interface DocFrontmatter {
53 | title?: string;
54 | description?: string;
55 | documcp?: DocFreshnessMetadata;
56 | [key: string]: unknown;
57 | }
58 |
59 | /**
60 | * File freshness status
61 | */
62 | export interface FileFreshnessStatus {
63 | filePath: string;
64 | relativePath: string;
65 | hasMetadata: boolean;
66 | metadata?: DocFreshnessMetadata;
67 | lastUpdated?: Date;
68 | lastValidated?: Date;
69 | ageInMs?: number;
70 | ageFormatted?: string;
71 | isStale: boolean;
72 | stalenessLevel: "fresh" | "warning" | "stale" | "critical" | "unknown";
73 | staleDays?: number;
74 | }
75 |
76 | /**
77 | * Freshness scan report
78 | */
79 | export interface FreshnessScanReport {
80 | scannedAt: string;
81 | docsPath: string;
82 | totalFiles: number;
83 | filesWithMetadata: number;
84 | filesWithoutMetadata: number;
85 | freshFiles: number;
86 | warningFiles: number;
87 | staleFiles: number;
88 | criticalFiles: number;
89 | files: FileFreshnessStatus[];
90 | thresholds: {
91 | warning: StalenessThreshold;
92 | stale: StalenessThreshold;
93 | critical: StalenessThreshold;
94 | };
95 | }
96 |
97 | /**
98 | * Convert time threshold to milliseconds
99 | */
100 | export function thresholdToMs(threshold: StalenessThreshold): number {
101 | const { value, unit } = threshold;
102 | switch (unit) {
103 | case "minutes":
104 | return value * 60 * 1000;
105 | case "hours":
106 | return value * 60 * 60 * 1000;
107 | case "days":
108 | return value * 24 * 60 * 60 * 1000;
109 | default:
110 | throw new Error(`Unknown time unit: ${unit}`);
111 | }
112 | }
113 |
114 | /**
115 | * Format age in human-readable format
116 | */
117 | export function formatAge(ageMs: number): string {
118 | const seconds = Math.floor(ageMs / 1000);
119 | const minutes = Math.floor(seconds / 60);
120 | const hours = Math.floor(minutes / 60);
121 | const days = Math.floor(hours / 24);
122 |
123 | if (days > 0) {
124 | return `${days} day${days !== 1 ? "s" : ""}`;
125 | } else if (hours > 0) {
126 | return `${hours} hour${hours !== 1 ? "s" : ""}`;
127 | } else if (minutes > 0) {
128 | return `${minutes} minute${minutes !== 1 ? "s" : ""}`;
129 | } else {
130 | return `${seconds} second${seconds !== 1 ? "s" : ""}`;
131 | }
132 | }
133 |
134 | /**
135 | * Parse frontmatter from markdown file
136 | */
137 | export async function parseDocFrontmatter(
138 | filePath: string,
139 | ): Promise<DocFrontmatter> {
140 | try {
141 | const content = await fs.readFile(filePath, "utf-8");
142 | const { data } = matter(content);
143 | return data as DocFrontmatter;
144 | } catch (error) {
145 | return {};
146 | }
147 | }
148 |
149 | /**
150 | * Update frontmatter in markdown file
151 | */
152 | export async function updateDocFrontmatter(
153 | filePath: string,
154 | metadata: Partial<DocFreshnessMetadata>,
155 | ): Promise<void> {
156 | const content = await fs.readFile(filePath, "utf-8");
157 | const { data, content: body } = matter(content);
158 |
159 | const existingDocuMCP = (data.documcp as DocFreshnessMetadata) || {};
160 | const updatedData = {
161 | ...data,
162 | documcp: {
163 | ...existingDocuMCP,
164 | ...metadata,
165 | },
166 | };
167 |
168 | const newContent = matter.stringify(body, updatedData);
169 | await fs.writeFile(filePath, newContent, "utf-8");
170 | }
171 |
172 | /**
173 | * Calculate file freshness status
174 | */
175 | export function calculateFreshnessStatus(
176 | filePath: string,
177 | relativePath: string,
178 | frontmatter: DocFrontmatter,
179 | thresholds: {
180 | warning: StalenessThreshold;
181 | stale: StalenessThreshold;
182 | critical: StalenessThreshold;
183 | },
184 | ): FileFreshnessStatus {
185 | const metadata = frontmatter.documcp;
186 | const hasMetadata = !!metadata?.last_updated;
187 |
188 | if (!hasMetadata) {
189 | return {
190 | filePath,
191 | relativePath,
192 | hasMetadata: false,
193 | isStale: true,
194 | stalenessLevel: "unknown",
195 | };
196 | }
197 |
198 | const lastUpdated = new Date(metadata.last_updated!);
199 | const lastValidated = metadata.last_validated
200 | ? new Date(metadata.last_validated)
201 | : undefined;
202 | const now = new Date();
203 | const ageInMs = now.getTime() - lastUpdated.getTime();
204 | const ageFormatted = formatAge(ageInMs);
205 | const staleDays = Math.floor(ageInMs / (24 * 60 * 60 * 1000));
206 |
207 | // Determine staleness level
208 | let stalenessLevel: FileFreshnessStatus["stalenessLevel"];
209 | let isStale: boolean;
210 |
211 | const warningMs = thresholdToMs(thresholds.warning);
212 | const staleMs = thresholdToMs(thresholds.stale);
213 | const criticalMs = thresholdToMs(thresholds.critical);
214 |
215 | if (ageInMs >= criticalMs) {
216 | stalenessLevel = "critical";
217 | isStale = true;
218 | } else if (ageInMs >= staleMs) {
219 | stalenessLevel = "stale";
220 | isStale = true;
221 | } else if (ageInMs >= warningMs) {
222 | stalenessLevel = "warning";
223 | isStale = false;
224 | } else {
225 | stalenessLevel = "fresh";
226 | isStale = false;
227 | }
228 |
229 | return {
230 | filePath,
231 | relativePath,
232 | hasMetadata: true,
233 | metadata,
234 | lastUpdated,
235 | lastValidated,
236 | ageInMs,
237 | ageFormatted,
238 | isStale,
239 | stalenessLevel,
240 | staleDays,
241 | };
242 | }
243 |
244 | /**
245 | * Find all markdown files in directory recursively
246 | */
247 | export async function findMarkdownFiles(dir: string): Promise<string[]> {
248 | const files: string[] = [];
249 |
250 | async function scan(currentDir: string): Promise<void> {
251 | const entries = await fs.readdir(currentDir, { withFileTypes: true });
252 |
253 | for (const entry of entries) {
254 | const fullPath = path.join(currentDir, entry.name);
255 |
256 | // Skip common directories
257 | if (entry.isDirectory()) {
258 | if (
259 | !["node_modules", ".git", "dist", "build", ".documcp"].includes(
260 | entry.name,
261 | )
262 | ) {
263 | await scan(fullPath);
264 | }
265 | continue;
266 | }
267 |
268 | // Include markdown files
269 | if (entry.isFile() && /\.(md|mdx)$/i.test(entry.name)) {
270 | files.push(fullPath);
271 | }
272 | }
273 | }
274 |
275 | await scan(dir);
276 | return files;
277 | }
278 |
279 | /**
280 | * Scan directory for documentation freshness
281 | */
282 | export async function scanDocumentationFreshness(
283 | docsPath: string,
284 | thresholds: {
285 | warning?: StalenessThreshold;
286 | stale?: StalenessThreshold;
287 | critical?: StalenessThreshold;
288 | } = {},
289 | ): Promise<FreshnessScanReport> {
290 | // Default thresholds
291 | const finalThresholds = {
292 | warning: thresholds.warning || STALENESS_PRESETS.weekly,
293 | stale: thresholds.stale || STALENESS_PRESETS.monthly,
294 | critical: thresholds.critical || STALENESS_PRESETS.quarterly,
295 | };
296 |
297 | // Find all markdown files
298 | const markdownFiles = await findMarkdownFiles(docsPath);
299 |
300 | // Analyze each file
301 | const files: FileFreshnessStatus[] = [];
302 | for (const filePath of markdownFiles) {
303 | const relativePath = path.relative(docsPath, filePath);
304 | const frontmatter = await parseDocFrontmatter(filePath);
305 | const status = calculateFreshnessStatus(
306 | filePath,
307 | relativePath,
308 | frontmatter,
309 | finalThresholds,
310 | );
311 | files.push(status);
312 | }
313 |
314 | // Calculate summary statistics
315 | const totalFiles = files.length;
316 | const filesWithMetadata = files.filter((f) => f.hasMetadata).length;
317 | const filesWithoutMetadata = totalFiles - filesWithMetadata;
318 | const freshFiles = files.filter((f) => f.stalenessLevel === "fresh").length;
319 | const warningFiles = files.filter(
320 | (f) => f.stalenessLevel === "warning",
321 | ).length;
322 | const staleFiles = files.filter((f) => f.stalenessLevel === "stale").length;
323 | const criticalFiles = files.filter(
324 | (f) => f.stalenessLevel === "critical",
325 | ).length;
326 |
327 | return {
328 | scannedAt: new Date().toISOString(),
329 | docsPath,
330 | totalFiles,
331 | filesWithMetadata,
332 | filesWithoutMetadata,
333 | freshFiles,
334 | warningFiles,
335 | staleFiles,
336 | criticalFiles,
337 | files,
338 | thresholds: finalThresholds,
339 | };
340 | }
341 |
342 | /**
343 | * Initialize frontmatter for files without metadata
344 | */
345 | export async function initializeFreshnessMetadata(
346 | filePath: string,
347 | options: {
348 | updateFrequency?: keyof typeof STALENESS_PRESETS;
349 | autoUpdated?: boolean;
350 | } = {},
351 | ): Promise<void> {
352 | const frontmatter = await parseDocFrontmatter(filePath);
353 |
354 | if (!frontmatter.documcp?.last_updated) {
355 | const metadata: DocFreshnessMetadata = {
356 | last_updated: new Date().toISOString(),
357 | last_validated: new Date().toISOString(),
358 | auto_updated: options.autoUpdated ?? false,
359 | update_frequency: options.updateFrequency || "monthly",
360 | };
361 |
362 | if (options.updateFrequency) {
363 | metadata.staleness_threshold = STALENESS_PRESETS[options.updateFrequency];
364 | }
365 |
366 | await updateDocFrontmatter(filePath, metadata);
367 | }
368 | }
369 |
```
--------------------------------------------------------------------------------
/tests/tools/readme-best-practices.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { readmeBestPractices } from "../../src/tools/readme-best-practices.js";
2 | import { formatMCPResponse } from "../../src/types/api.js";
3 | import { writeFile, mkdir, rm } from "fs/promises";
4 | import { join } from "path";
5 |
6 | describe("readmeBestPractices", () => {
7 | const testDir = join(process.cwd(), "test-readme-best-practices-temp");
8 |
9 | beforeEach(async () => {
10 | // Create test directory
11 | await mkdir(testDir, { recursive: true });
12 | });
13 |
14 | afterEach(async () => {
15 | // Clean up test directory
16 | try {
17 | await rm(testDir, { recursive: true, force: true });
18 | } catch (error) {
19 | // Ignore cleanup errors
20 | }
21 | });
22 |
23 | describe("Basic Functionality", () => {
24 | test("should analyze README best practices with default parameters", async () => {
25 | const readmePath = join(testDir, "README.md");
26 | await writeFile(
27 | readmePath,
28 | `# Test Library
29 |
30 | ## Description
31 | This is a test library for analyzing best practices.
32 |
33 | ## Installation
34 | \`\`\`bash
35 | npm install test-library
36 | \`\`\`
37 |
38 | ## Usage
39 | \`\`\`javascript
40 | const lib = require('test-library');
41 | \`\`\`
42 |
43 | ## API Reference
44 | Function documentation here.
45 |
46 | ## Contributing
47 | Please read CONTRIBUTING.md.
48 |
49 | ## License
50 | MIT License
51 | `,
52 | );
53 |
54 | const result = await readmeBestPractices({
55 | readme_path: readmePath,
56 | });
57 |
58 | expect(result.success).toBe(true);
59 | expect(result.data).toBeDefined();
60 | expect(result.data!.bestPracticesReport).toBeDefined();
61 | expect(result.metadata).toBeDefined();
62 | });
63 |
64 | test("should handle different project types", async () => {
65 | const readmePath = join(testDir, "README.md");
66 | await writeFile(readmePath, "# Application\n\nA web application.");
67 |
68 | const result = await readmeBestPractices({
69 | readme_path: readmePath,
70 | project_type: "application",
71 | });
72 |
73 | expect(result.success).toBe(true);
74 | expect(result.data).toBeDefined();
75 | });
76 |
77 | test("should generate templates when requested", async () => {
78 | const outputDir = join(testDir, "output");
79 | await mkdir(outputDir, { recursive: true });
80 |
81 | const result = await readmeBestPractices({
82 | readme_path: join(testDir, "nonexistent.md"),
83 | generate_template: true,
84 | output_directory: outputDir,
85 | project_type: "library",
86 | });
87 |
88 | expect(result.success).toBe(true);
89 | expect(result.data).toBeDefined();
90 | });
91 |
92 | test("should handle different target audiences", async () => {
93 | const readmePath = join(testDir, "README.md");
94 | await writeFile(readmePath, "# Advanced Tool\n\nFor expert users.");
95 |
96 | const result = await readmeBestPractices({
97 | readme_path: readmePath,
98 | target_audience: "advanced",
99 | });
100 |
101 | expect(result.success).toBe(true);
102 | expect(result.data).toBeDefined();
103 | });
104 | });
105 |
106 | describe("Error Handling", () => {
107 | test("should handle missing README file without template generation", async () => {
108 | const result = await readmeBestPractices({
109 | readme_path: join(testDir, "nonexistent.md"),
110 | generate_template: false,
111 | });
112 |
113 | expect(result.success).toBe(false);
114 | expect(result.error).toBeDefined();
115 | expect(result.error!.code).toBe("README_NOT_FOUND");
116 | });
117 |
118 | test("should handle invalid project type", async () => {
119 | const readmePath = join(testDir, "README.md");
120 | await writeFile(readmePath, "# Test");
121 |
122 | const result = await readmeBestPractices({
123 | readme_path: readmePath,
124 | project_type: "invalid_type" as any,
125 | });
126 |
127 | expect(result.success).toBe(false);
128 | expect(result.error).toBeDefined();
129 | });
130 |
131 | test("should handle invalid target audience", async () => {
132 | const readmePath = join(testDir, "README.md");
133 | await writeFile(readmePath, "# Test");
134 |
135 | const result = await readmeBestPractices({
136 | readme_path: readmePath,
137 | target_audience: "invalid_audience" as any,
138 | });
139 |
140 | expect(result.success).toBe(false);
141 | expect(result.error).toBeDefined();
142 | });
143 | });
144 |
145 | describe("Best Practices Analysis", () => {
146 | test("should evaluate checklist items", async () => {
147 | const readmePath = join(testDir, "README.md");
148 | await writeFile(
149 | readmePath,
150 | `# Complete Library
151 |
152 | ## Table of Contents
153 | - [Installation](#installation)
154 | - [Usage](#usage)
155 |
156 | ## Description
157 | Detailed description of the library.
158 |
159 | ## Installation
160 | Installation instructions here.
161 |
162 | ## Usage
163 | Usage examples here.
164 |
165 | ## API Reference
166 | API documentation.
167 |
168 | ## Examples
169 | Code examples.
170 |
171 | ## Contributing
172 | Contributing guidelines.
173 |
174 | ## License
175 | MIT License
176 |
177 | ## Support
178 | Support information.
179 | `,
180 | );
181 |
182 | const result = await readmeBestPractices({
183 | readme_path: readmePath,
184 | project_type: "library",
185 | });
186 |
187 | expect(result.success).toBe(true);
188 | expect(result.data!.bestPracticesReport.checklist).toBeDefined();
189 | expect(Array.isArray(result.data!.bestPracticesReport.checklist)).toBe(
190 | true,
191 | );
192 | expect(result.data!.bestPracticesReport.checklist.length).toBeGreaterThan(
193 | 0,
194 | );
195 | });
196 |
197 | test("should calculate overall score and grade", async () => {
198 | const readmePath = join(testDir, "README.md");
199 | await writeFile(readmePath, "# Basic Project\n\nMinimal content.");
200 |
201 | const result = await readmeBestPractices({
202 | readme_path: readmePath,
203 | });
204 |
205 | expect(result.success).toBe(true);
206 | expect(
207 | result.data!.bestPracticesReport.overallScore,
208 | ).toBeGreaterThanOrEqual(0);
209 | expect(result.data!.bestPracticesReport.overallScore).toBeLessThanOrEqual(
210 | 100,
211 | );
212 | expect(result.data!.bestPracticesReport.grade).toBeDefined();
213 | });
214 |
215 | test("should provide recommendations", async () => {
216 | const readmePath = join(testDir, "README.md");
217 | await writeFile(readmePath, "# Incomplete Project");
218 |
219 | const result = await readmeBestPractices({
220 | readme_path: readmePath,
221 | });
222 |
223 | expect(result.success).toBe(true);
224 | expect(result.data!.recommendations).toBeDefined();
225 | expect(Array.isArray(result.data!.recommendations)).toBe(true);
226 | expect(result.data!.nextSteps).toBeDefined();
227 | expect(Array.isArray(result.data!.nextSteps)).toBe(true);
228 | });
229 |
230 | test("should provide summary metrics", async () => {
231 | const readmePath = join(testDir, "README.md");
232 | await writeFile(
233 | readmePath,
234 | `# Project
235 |
236 | ## Description
237 | Basic description.
238 |
239 | ## Installation
240 | Install steps.
241 | `,
242 | );
243 |
244 | const result = await readmeBestPractices({
245 | readme_path: readmePath,
246 | });
247 |
248 | expect(result.success).toBe(true);
249 | expect(result.data!.bestPracticesReport.summary).toBeDefined();
250 | expect(
251 | result.data!.bestPracticesReport.summary.criticalIssues,
252 | ).toBeGreaterThanOrEqual(0);
253 | expect(
254 | result.data!.bestPracticesReport.summary.importantIssues,
255 | ).toBeGreaterThanOrEqual(0);
256 | expect(
257 | result.data!.bestPracticesReport.summary.sectionsPresent,
258 | ).toBeGreaterThanOrEqual(0);
259 | expect(
260 | result.data!.bestPracticesReport.summary.totalSections,
261 | ).toBeGreaterThan(0);
262 | });
263 | });
264 |
265 | describe("Template Generation", () => {
266 | test("should generate README template when file is missing", async () => {
267 | const outputDir = join(testDir, "template-output");
268 | await mkdir(outputDir, { recursive: true });
269 |
270 | const result = await readmeBestPractices({
271 | readme_path: join(testDir, "missing.md"),
272 | generate_template: true,
273 | output_directory: outputDir,
274 | project_type: "tool",
275 | include_community_files: true,
276 | });
277 |
278 | expect(result.success).toBe(true);
279 | expect(result.data).toBeDefined();
280 | });
281 |
282 | test("should handle template generation without community files", async () => {
283 | const outputDir = join(testDir, "no-community-output");
284 | await mkdir(outputDir, { recursive: true });
285 |
286 | const result = await readmeBestPractices({
287 | readme_path: join(testDir, "missing.md"),
288 | generate_template: true,
289 | output_directory: outputDir,
290 | include_community_files: false,
291 | });
292 |
293 | expect(result.success).toBe(true);
294 | expect(result.data).toBeDefined();
295 | });
296 | });
297 |
298 | describe("Response Format", () => {
299 | test("should return MCPToolResponse structure", async () => {
300 | const readmePath = join(testDir, "README.md");
301 | await writeFile(readmePath, "# Test Project");
302 |
303 | const result = await readmeBestPractices({
304 | readme_path: readmePath,
305 | });
306 |
307 | expect(result.success).toBeDefined();
308 | expect(result.metadata).toBeDefined();
309 | expect(result.metadata.toolVersion).toBe("1.0.0");
310 | expect(result.metadata.executionTime).toBeGreaterThanOrEqual(0);
311 | expect(result.metadata.timestamp).toBeDefined();
312 | expect(result.metadata.analysisId).toBeDefined();
313 | });
314 |
315 | test("should format properly with formatMCPResponse", async () => {
316 | const readmePath = join(testDir, "README.md");
317 | await writeFile(readmePath, "# Test Project");
318 |
319 | const result = await readmeBestPractices({
320 | readme_path: readmePath,
321 | });
322 |
323 | // Test that the result can be formatted without errors
324 | const formatted = formatMCPResponse(result);
325 | expect(formatted.content).toBeDefined();
326 | expect(Array.isArray(formatted.content)).toBe(true);
327 | expect(formatted.content.length).toBeGreaterThan(0);
328 | expect(formatted.isError).toBe(false);
329 | });
330 | });
331 | });
332 |
```
--------------------------------------------------------------------------------
/docs/adrs/adr-0002-repository-analysis-engine.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | id: adr-2-repository-analysis-engine
3 | title: "ADR-002: Repository Analysis Engine Design"
4 | sidebar_label: "ADR-002: Repository Analysis Engine Design"
5 | sidebar_position: 2
6 | documcp:
7 | last_updated: "2025-01-14T00:00:00.000Z"
8 | last_validated: "2025-01-14T00:00:00.000Z"
9 | auto_updated: false
10 | update_frequency: monthly
11 | validated_against_commit: 577a312
12 | ---
13 |
14 | # ADR-002: Multi-Layered Repository Analysis Engine Design
15 |
16 | ## Status
17 |
18 | Accepted
19 |
20 | ## Context
21 |
22 | DocuMCP needs to understand repository characteristics to make intelligent recommendations about static site generators and documentation structure. The analysis must go beyond simple file counting to provide deep insights into project complexity, language ecosystems, existing documentation patterns, and development practices.
23 |
24 | Key requirements:
25 |
26 | - Comprehensive project characterization
27 | - Language ecosystem detection
28 | - Documentation quality assessment
29 | - Project complexity evaluation
30 | - Performance optimization for large repositories
31 | - Extensible architecture for new analysis types
32 |
33 | ## Decision
34 |
35 | We will implement a multi-layered repository analysis engine that examines repositories from multiple perspectives to build comprehensive project profiles.
36 |
37 | ### Analysis Layers:
38 |
39 | #### 1. File System Analysis Layer
40 |
41 | - **Recursive directory traversal** with intelligent filtering
42 | - **File categorization** by extension and content patterns
43 | - **Metrics calculation**: file counts, lines of code, directory depth, size distributions
44 | - **Ignore pattern handling**: .gitignore, common build artifacts, node_modules
45 |
46 | #### 2. Language Ecosystem Analysis Layer
47 |
48 | - **Package manager detection**: package.json, requirements.txt, Cargo.toml, go.mod, etc.
49 | - **Dependency analysis**: direct and transitive dependencies
50 | - **Build tool identification**: webpack, vite, gradle, maven, cargo, etc.
51 | - **Version constraint analysis**: compatibility requirements
52 |
53 | #### 3. Content Analysis Layer
54 |
55 | - **Documentation quality assessment**: README analysis, existing docs
56 | - **Code comment analysis**: inline documentation patterns
57 | - **API surface detection**: public interfaces, exported functions
58 | - **Content gap identification**: missing documentation areas
59 |
60 | #### 4. Project Metadata Analysis Layer
61 |
62 | - **Git history patterns**: commit frequency, contributor activity
63 | - **Release management**: tagging patterns, version schemes
64 | - **Issue tracking**: GitHub issues, project management indicators
65 | - **Community engagement**: contributor count, activity patterns
66 |
67 | #### 5. Complexity Assessment Layer
68 |
69 | - **Architectural complexity**: microservices, modular design patterns
70 | - **Technical complexity**: multi-language projects, advanced configurations
71 | - **Maintenance indicators**: test coverage, CI/CD presence, code quality metrics
72 | - **Documentation sophistication needs**: API complexity, user journey complexity
73 |
74 | ## Alternatives Considered
75 |
76 | ### Single-Pass Analysis
77 |
78 | - **Pros**: Simpler implementation, faster for small repositories
79 | - **Cons**: Limited depth, cannot build sophisticated project profiles
80 | - **Decision**: Rejected due to insufficient intelligence for quality recommendations
81 |
82 | ### External Tool Integration (e.g., GitHub API, CodeClimate)
83 |
84 | - **Pros**: Rich metadata, established metrics
85 | - **Cons**: External dependencies, rate limiting, requires authentication
86 | - **Decision**: Rejected for core analysis; may integrate as optional enhancement
87 |
88 | ### Machine Learning-Based Analysis
89 |
90 | - **Pros**: Could learn patterns from successful documentation projects
91 | - **Cons**: Training data requirements, model maintenance, unpredictable results
92 | - **Decision**: Deferred to future versions; start with rule-based analysis
93 |
94 | ### Database-Backed Caching
95 |
96 | - **Pros**: Faster repeat analysis, could store learning patterns
97 | - **Cons**: Deployment complexity, staleness issues, synchronization problems
98 | - **Decision**: Rejected for initial version; implement in-memory caching only
99 |
100 | ## Consequences
101 |
102 | ### Positive
103 |
104 | - **Intelligent Recommendations**: Deep analysis enables sophisticated SSG matching
105 | - **Extensible Architecture**: Easy to add new analysis dimensions
106 | - **Performance Optimization**: Layered approach allows selective analysis depth
107 | - **Quality Assessment**: Can identify and improve existing documentation
108 | - **Future-Proof**: Architecture supports ML integration and advanced analytics
109 |
110 | ### Negative
111 |
112 | - **Analysis Time**: Comprehensive analysis may be slower for large repositories
113 | - **Complexity**: Multi-layered architecture requires careful coordination
114 | - **Memory Usage**: Full repository analysis requires significant memory for large projects
115 |
116 | ### Risks and Mitigations
117 |
118 | - **Performance**: Implement streaming analysis and configurable depth limits
119 | - **Accuracy**: Validate analysis results against known project types
120 | - **Maintenance**: Regular testing against diverse repository types
121 |
122 | ## Implementation Details
123 |
124 | ### Analysis Engine Structure
125 |
126 | ```typescript
127 | interface RepositoryAnalysis {
128 | fileSystem: FileSystemAnalysis;
129 | languageEcosystem: LanguageEcosystemAnalysis;
130 | content: ContentAnalysis;
131 | metadata: ProjectMetadataAnalysis;
132 | complexity: ComplexityAssessment;
133 | }
134 |
135 | interface AnalysisLayer {
136 | analyze(repositoryPath: string): Promise<LayerResult>;
137 | getMetrics(): AnalysisMetrics;
138 | validate(): ValidationResult;
139 | }
140 | ```
141 |
142 | ### Performance Optimizations
143 |
144 | - **Parallel Analysis**: Independent layers run concurrently
145 | - **Intelligent Filtering**: Skip irrelevant files and directories early
146 | - **Progressive Analysis**: Start with lightweight analysis, deepen as needed
147 | - **Caching Strategy**: Cache analysis results within session scope
148 | - **Size Limits**: Configurable limits for very large repositories
149 |
150 | ### File Pattern Recognition
151 |
152 | ```typescript
153 | const FILE_PATTERNS = {
154 | documentation: [".md", ".rst", ".adoc", "docs/", "documentation/"],
155 | configuration: ["config/", ".config/", "*.json", "*.yaml", "*.toml"],
156 | source: ["src/", "lib/", "*.js", "*.ts", "*.py", "*.go", "*.rs"],
157 | tests: ["test/", "tests/", "__tests__/", "*.test.*", "*.spec.*"],
158 | build: ["build/", "dist/", "target/", "bin/", "*.lock"],
159 | };
160 | ```
161 |
162 | ### Language Ecosystem Detection
163 |
164 | ```typescript
165 | const ECOSYSTEM_INDICATORS = {
166 | javascript: ["package.json", "node_modules/", "yarn.lock", "pnpm-lock.yaml"],
167 | python: ["requirements.txt", "setup.py", "pyproject.toml", "Pipfile"],
168 | rust: ["Cargo.toml", "Cargo.lock", "src/main.rs"],
169 | go: ["go.mod", "go.sum", "main.go"],
170 | java: ["pom.xml", "build.gradle", "gradlew"],
171 | };
172 | ```
173 |
174 | ### Complexity Scoring Algorithm
175 |
176 | ```typescript
177 | interface ComplexityFactors {
178 | fileCount: number;
179 | languageCount: number;
180 | dependencyCount: number;
181 | directoryDepth: number;
182 | contributorCount: number;
183 | apiSurfaceSize: number;
184 | }
185 |
186 | function calculateComplexityScore(factors: ComplexityFactors): ComplexityScore {
187 | // Weighted scoring algorithm balancing multiple factors
188 | // Returns: 'simple' | 'moderate' | 'complex' | 'enterprise'
189 | }
190 | ```
191 |
192 | ## Quality Assurance
193 |
194 | ### Testing Strategy
195 |
196 | - **Unit Tests**: Each analysis layer tested independently
197 | - **Integration Tests**: Full analysis pipeline validation
198 | - **Repository Fixtures**: Test suite with diverse project types
199 | - **Performance Tests**: Analysis time benchmarks for various repository sizes
200 | - **Accuracy Validation**: Manual verification against known project characteristics
201 |
202 | ### Monitoring and Metrics
203 |
204 | - Analysis execution time by repository size
205 | - Accuracy of complexity assessments
206 | - Cache hit rates and memory usage
207 | - Error rates and failure modes
208 |
209 | ## Future Enhancements
210 |
211 | ### Machine Learning Integration
212 |
213 | - Pattern recognition for project types
214 | - Automated documentation quality scoring
215 | - Predictive analysis for maintenance needs
216 |
217 | ### Advanced Analytics
218 |
219 | - Historical trend analysis
220 | - Comparative analysis across similar projects
221 | - Community best practice identification
222 |
223 | ### Performance Optimizations
224 |
225 | - WebAssembly modules for intensive analysis
226 | - Distributed analysis for very large repositories
227 | - Incremental analysis for updated repositories
228 |
229 | ## Security Considerations
230 |
231 | - **File System Access**: Restricted to repository boundaries
232 | - **Content Scanning**: No sensitive data extraction or storage
233 | - **Resource Limits**: Prevent resource exhaustion attacks
234 | - **Input Validation**: Sanitize all repository paths and content
235 |
236 | ## Implementation Status
237 |
238 | **Status**: ✅ Implemented (2025-12-12)
239 |
240 | **Implementation Files**:
241 |
242 | - `src/tools/analyze-repository.ts` - Main repository analysis tool
243 | - `src/utils/code-scanner.ts` - Code scanning and analysis utilities
244 | - `src/memory/knowledge-graph.ts` - Knowledge graph integration for storing analysis results
245 |
246 | **Key Features Implemented**:
247 |
248 | - ✅ Multi-layered analysis (file system, language ecosystem, content, metadata, complexity)
249 | - ✅ Dependency detection and analysis
250 | - ✅ Documentation quality assessment
251 | - ✅ Project complexity evaluation
252 | - ✅ Knowledge graph integration for historical tracking
253 | - ✅ Progress reporting and context-aware analysis
254 |
255 | **Validation**: The implementation has been validated against the architectural design and is actively used by other tools (SSG recommendation, content population, drift detection).
256 |
257 | ## References
258 |
259 | - [Git Repository Analysis Best Practices](https://git-scm.com/docs)
260 | - [Static Analysis Tools Comparison](https://analysis-tools.dev/)
261 | - [Repository Metrics Standards](https://chaoss.community/)
262 | - Commit: 577a312 - feat: Extend knowledge graph with documentation example entities (#78)
263 | - GitHub Issue: #77 - Knowledge graph extensions (referenced in commit)
264 | - GitHub Issue: #78 - Extend knowledge graph with documentation example entities
265 |
```
--------------------------------------------------------------------------------
/src/benchmarks/performance.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Performance benchmarking system per PERF-001 rules
2 | import { promises as fs } from "fs";
3 | import path from "path";
4 | import { analyzeRepository } from "../tools/analyze-repository.js";
5 |
6 | export interface BenchmarkResult {
7 | repoSize: "small" | "medium" | "large";
8 | fileCount: number;
9 | executionTime: number;
10 | targetTime: number;
11 | passed: boolean;
12 | performanceRatio: number;
13 | details: {
14 | startTime: number;
15 | endTime: number;
16 | memoryUsage: NodeJS.MemoryUsage;
17 | };
18 | }
19 |
20 | export interface BenchmarkSuite {
21 | testName: string;
22 | results: BenchmarkResult[];
23 | overallPassed: boolean;
24 | averagePerformance: number;
25 | summary: {
26 | smallRepos: { count: number; avgTime: number; passed: number };
27 | mediumRepos: { count: number; avgTime: number; passed: number };
28 | largeRepos: { count: number; avgTime: number; passed: number };
29 | };
30 | }
31 |
32 | // PERF-001 performance targets
33 | const PERFORMANCE_TARGETS = {
34 | small: 1000, // <1 second for <100 files
35 | medium: 10000, // <10 seconds for 100-1000 files
36 | large: 60000, // <60 seconds for 1000+ files
37 | } as const;
38 |
39 | export class PerformanceBenchmarker {
40 | private results: BenchmarkResult[] = [];
41 |
42 | async benchmarkRepository(
43 | repoPath: string,
44 | depth: "quick" | "standard" | "deep" = "standard",
45 | ): Promise<BenchmarkResult> {
46 | const fileCount = await this.getFileCount(repoPath);
47 | const repoSize = this.categorizeRepoSize(fileCount);
48 | const targetTime = PERFORMANCE_TARGETS[repoSize];
49 |
50 | // Capture initial memory state
51 | const initialMemory = process.memoryUsage();
52 |
53 | const startTime = Date.now();
54 |
55 | try {
56 | // Run the actual analysis
57 | await analyzeRepository({ path: repoPath, depth });
58 |
59 | const endTime = Date.now();
60 | const executionTime = endTime - startTime;
61 | const finalMemory = process.memoryUsage();
62 |
63 | const performanceRatio = executionTime / targetTime;
64 | const passed = executionTime <= targetTime;
65 |
66 | const result: BenchmarkResult = {
67 | repoSize,
68 | fileCount,
69 | executionTime,
70 | targetTime,
71 | passed,
72 | performanceRatio,
73 | details: {
74 | startTime,
75 | endTime,
76 | memoryUsage: {
77 | rss: finalMemory.rss - initialMemory.rss,
78 | heapTotal: finalMemory.heapTotal - initialMemory.heapTotal,
79 | heapUsed: finalMemory.heapUsed - initialMemory.heapUsed,
80 | external: finalMemory.external - initialMemory.external,
81 | arrayBuffers: finalMemory.arrayBuffers - initialMemory.arrayBuffers,
82 | },
83 | },
84 | };
85 |
86 | this.results.push(result);
87 | return result;
88 | } catch (error) {
89 | const endTime = Date.now();
90 | const executionTime = endTime - startTime;
91 |
92 | // Even failed executions should be benchmarked
93 | const result: BenchmarkResult = {
94 | repoSize,
95 | fileCount,
96 | executionTime,
97 | targetTime,
98 | passed: false, // Failed execution = failed performance
99 | performanceRatio: executionTime / targetTime,
100 | details: {
101 | startTime,
102 | endTime,
103 | memoryUsage: process.memoryUsage(),
104 | },
105 | };
106 |
107 | this.results.push(result);
108 | throw error;
109 | }
110 | }
111 |
112 | async runBenchmarkSuite(
113 | testRepos: Array<{ path: string; name: string }>,
114 | ): Promise<BenchmarkSuite> {
115 | console.log("🚀 Starting performance benchmark suite...\n");
116 |
117 | const results: BenchmarkResult[] = [];
118 |
119 | for (const repo of testRepos) {
120 | console.log(`📊 Benchmarking: ${repo.name}`);
121 |
122 | try {
123 | const result = await this.benchmarkRepository(repo.path);
124 | results.push(result);
125 |
126 | const status = result.passed ? "✅ PASS" : "❌ FAIL";
127 | const ratio = (result.performanceRatio * 100).toFixed(1);
128 |
129 | console.log(
130 | ` ${status} ${result.executionTime}ms (${ratio}% of target) - ${result.repoSize} repo with ${result.fileCount} files`,
131 | );
132 | } catch (error) {
133 | console.log(` ❌ ERROR: ${error}`);
134 | }
135 | }
136 |
137 | console.log("\n📈 Generating performance summary...\n");
138 |
139 | return this.generateSuite("Full Benchmark Suite", results);
140 | }
141 |
142 | generateSuite(testName: string, results: BenchmarkResult[]): BenchmarkSuite {
143 | const overallPassed = results.every((r) => r.passed);
144 | const averagePerformance =
145 | results.reduce((sum, r) => sum + r.performanceRatio, 0) / results.length;
146 |
147 | // Categorize results
148 | const smallRepos = results.filter((r) => r.repoSize === "small");
149 | const mediumRepos = results.filter((r) => r.repoSize === "medium");
150 | const largeRepos = results.filter((r) => r.repoSize === "large");
151 |
152 | const suite: BenchmarkSuite = {
153 | testName,
154 | results,
155 | overallPassed,
156 | averagePerformance,
157 | summary: {
158 | smallRepos: {
159 | count: smallRepos.length,
160 | avgTime:
161 | smallRepos.reduce((sum, r) => sum + r.executionTime, 0) /
162 | smallRepos.length || 0,
163 | passed: smallRepos.filter((r) => r.passed).length,
164 | },
165 | mediumRepos: {
166 | count: mediumRepos.length,
167 | avgTime:
168 | mediumRepos.reduce((sum, r) => sum + r.executionTime, 0) /
169 | mediumRepos.length || 0,
170 | passed: mediumRepos.filter((r) => r.passed).length,
171 | },
172 | largeRepos: {
173 | count: largeRepos.length,
174 | avgTime:
175 | largeRepos.reduce((sum, r) => sum + r.executionTime, 0) /
176 | largeRepos.length || 0,
177 | passed: largeRepos.filter((r) => r.passed).length,
178 | },
179 | },
180 | };
181 |
182 | return suite;
183 | }
184 |
185 | printDetailedReport(suite: BenchmarkSuite): void {
186 | console.log(`📋 Performance Benchmark Report: ${suite.testName}`);
187 | console.log("=".repeat(60));
188 | console.log(
189 | `Overall Status: ${suite.overallPassed ? "✅ PASSED" : "❌ FAILED"}`,
190 | );
191 | console.log(
192 | `Average Performance: ${(suite.averagePerformance * 100).toFixed(
193 | 1,
194 | )}% of target`,
195 | );
196 | console.log(`Total Tests: ${suite.results.length}\n`);
197 |
198 | // Summary by repo size
199 | console.log("📊 Performance by Repository Size:");
200 | console.log("-".repeat(40));
201 |
202 | const categories = [
203 | {
204 | name: "Small (<100 files)",
205 | data: suite.summary.smallRepos,
206 | target: PERFORMANCE_TARGETS.small,
207 | },
208 | {
209 | name: "Medium (100-1000 files)",
210 | data: suite.summary.mediumRepos,
211 | target: PERFORMANCE_TARGETS.medium,
212 | },
213 | {
214 | name: "Large (1000+ files)",
215 | data: suite.summary.largeRepos,
216 | target: PERFORMANCE_TARGETS.large,
217 | },
218 | ];
219 |
220 | categories.forEach((cat) => {
221 | if (cat.data.count > 0) {
222 | const passRate = ((cat.data.passed / cat.data.count) * 100).toFixed(1);
223 | const avgTime = cat.data.avgTime.toFixed(0);
224 | const targetTime = (cat.target / 1000).toFixed(1);
225 |
226 | console.log(`${cat.name}:`);
227 | console.log(
228 | ` Tests: ${cat.data.count} | Passed: ${cat.data.passed}/${cat.data.count} (${passRate}%)`,
229 | );
230 | console.log(` Avg Time: ${avgTime}ms | Target: <${targetTime}s`);
231 | console.log("");
232 | }
233 | });
234 |
235 | // Detailed results
236 | console.log("🔍 Detailed Results:");
237 | console.log("-".repeat(40));
238 |
239 | suite.results.forEach((result, i) => {
240 | const status = result.passed ? "✅" : "❌";
241 | const ratio = (result.performanceRatio * 100).toFixed(1);
242 | const memoryMB = (
243 | result.details.memoryUsage.heapUsed /
244 | 1024 /
245 | 1024
246 | ).toFixed(1);
247 |
248 | console.log(
249 | `${status} Test ${i + 1}: ${
250 | result.executionTime
251 | }ms (${ratio}% of target)`,
252 | );
253 | console.log(
254 | ` Size: ${result.repoSize} (${result.fileCount} files) | Memory: ${memoryMB}MB heap`,
255 | );
256 | });
257 |
258 | console.log("\n" + "=".repeat(60));
259 | }
260 |
261 | exportResults(suite: BenchmarkSuite, outputPath: string): Promise<void> {
262 | const report = {
263 | timestamp: new Date().toISOString(),
264 | suite,
265 | systemInfo: {
266 | node: process.version,
267 | platform: process.platform,
268 | arch: process.arch,
269 | memoryUsage: process.memoryUsage(),
270 | },
271 | performanceTargets: PERFORMANCE_TARGETS,
272 | };
273 |
274 | return fs.writeFile(outputPath, JSON.stringify(report, null, 2));
275 | }
276 |
277 | private async getFileCount(repoPath: string): Promise<number> {
278 | let fileCount = 0;
279 |
280 | async function countFiles(dir: string, level = 0): Promise<void> {
281 | if (level > 10) return; // Prevent infinite recursion
282 |
283 | try {
284 | const entries = await fs.readdir(dir, { withFileTypes: true });
285 |
286 | for (const entry of entries) {
287 | if (entry.name.startsWith(".") && entry.name !== ".github") continue;
288 | if (entry.name === "node_modules" || entry.name === "vendor")
289 | continue;
290 |
291 | const fullPath = path.join(dir, entry.name);
292 |
293 | if (entry.isDirectory()) {
294 | await countFiles(fullPath, level + 1);
295 | } else {
296 | fileCount++;
297 | }
298 | }
299 | } catch (error) {
300 | // Skip inaccessible directories
301 | }
302 | }
303 |
304 | await countFiles(repoPath);
305 | return fileCount;
306 | }
307 |
308 | private categorizeRepoSize(fileCount: number): "small" | "medium" | "large" {
309 | if (fileCount < 100) return "small";
310 | if (fileCount < 1000) return "medium";
311 | return "large";
312 | }
313 |
314 | // Utility method to clear results for fresh benchmarking
315 | reset(): void {
316 | this.results = [];
317 | }
318 |
319 | // Get current benchmark results
320 | getResults(): BenchmarkResult[] {
321 | return [...this.results];
322 | }
323 | }
324 |
325 | // Factory function for easy usage
326 | export function createBenchmarker(): PerformanceBenchmarker {
327 | return new PerformanceBenchmarker();
328 | }
329 |
```