This is page 25 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
--------------------------------------------------------------------------------
/src/tools/update-existing-documentation.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
2 | import * as fs from "fs/promises";
3 | import * as path from "path";
4 | import {
5 | handleMemoryRecall,
6 | handleMemoryEnhancedRecommendation,
7 | handleMemoryIntelligentAnalysis,
8 | } from "../memory/index.js";
9 |
10 | interface UpdateOptions {
11 | analysisId: string;
12 | docsPath: string;
13 | compareMode: "comprehensive" | "gap-detection" | "accuracy-check";
14 | updateStrategy: "conservative" | "moderate" | "aggressive";
15 | preserveStyle: boolean;
16 | focusAreas?: string[];
17 | }
18 |
19 | interface DocumentationGap {
20 | type: "missing" | "outdated" | "incorrect" | "incomplete";
21 | location: string;
22 | description: string;
23 | severity: "low" | "medium" | "high" | "critical";
24 | suggestedUpdate: string;
25 | memoryEvidence?: any[];
26 | }
27 |
28 | interface CodeDocumentationComparison {
29 | codeFeatures: any[];
30 | documentedFeatures: any[];
31 | gaps: DocumentationGap[];
32 | outdatedSections: any[];
33 | accuracyIssues: any[];
34 | }
35 |
36 | interface UpdateRecommendation {
37 | section: string;
38 | currentContent: string;
39 | suggestedContent: string;
40 | reasoning: string;
41 | memoryEvidence: any[];
42 | confidence: number;
43 | effort: "low" | "medium" | "high";
44 | }
45 |
46 | interface UpdateResult {
47 | success: boolean;
48 | analysisPerformed: CodeDocumentationComparison;
49 | recommendations: UpdateRecommendation[];
50 | memoryInsights: {
51 | similarProjects: any[];
52 | successfulUpdatePatterns: any[];
53 | commonGapTypes: Record<string, number>;
54 | };
55 | updateMetrics: {
56 | gapsDetected: number;
57 | recommendationsGenerated: number;
58 | confidenceScore: number;
59 | estimatedEffort: string;
60 | };
61 | nextSteps: string[];
62 | }
63 |
64 | class DocumentationUpdateEngine {
65 | private memoryInsights: any = null;
66 | private codeAnalysis: any = null;
67 | private existingDocs: Map<string, any> = new Map();
68 |
69 | async updateExistingDocumentation(
70 | options: UpdateOptions,
71 | ): Promise<UpdateResult> {
72 | // 1. Load repository analysis and memory insights
73 | const analysis = await this.getRepositoryAnalysis(options.analysisId);
74 | this.codeAnalysis = analysis;
75 |
76 | // 2. Load memory insights for intelligent comparison
77 | await this.loadMemoryInsights(analysis, options);
78 |
79 | // 3. Analyze existing documentation structure and content
80 | const existingDocs = await this.analyzeExistingDocumentation(
81 | options.docsPath,
82 | );
83 | this.existingDocs = existingDocs;
84 |
85 | // 4. Perform comprehensive code-documentation comparison
86 | const comparison = await this.performCodeDocumentationComparison(
87 | analysis,
88 | existingDocs,
89 | options,
90 | );
91 |
92 | // 5. Generate memory-informed update recommendations
93 | const recommendations = await this.generateUpdateRecommendations(
94 | comparison,
95 | options,
96 | );
97 |
98 | // 6. Calculate metrics and confidence scores
99 | const updateMetrics = this.calculateUpdateMetrics(
100 | comparison,
101 | recommendations,
102 | );
103 |
104 | return {
105 | success: true,
106 | analysisPerformed: comparison,
107 | recommendations,
108 | memoryInsights: this.memoryInsights,
109 | updateMetrics,
110 | nextSteps: this.generateMemoryInformedNextSteps(
111 | comparison,
112 | recommendations,
113 | ),
114 | };
115 | }
116 |
117 | private async getRepositoryAnalysis(analysisId: string): Promise<any> {
118 | // Try to get analysis from memory system first
119 | try {
120 | const memoryRecall = await handleMemoryRecall({
121 | query: analysisId,
122 | type: "analysis",
123 | limit: 1,
124 | });
125 |
126 | // Handle the memory recall result structure
127 | if (
128 | memoryRecall &&
129 | memoryRecall.memories &&
130 | memoryRecall.memories.length > 0
131 | ) {
132 | const memory = memoryRecall.memories[0];
133 |
134 | // Handle wrapped content structure
135 | if (
136 | memory.data &&
137 | memory.data.content &&
138 | Array.isArray(memory.data.content)
139 | ) {
140 | // Extract the JSON from the first text content
141 | const firstContent = memory.data.content[0];
142 | if (
143 | firstContent &&
144 | firstContent.type === "text" &&
145 | firstContent.text
146 | ) {
147 | try {
148 | return JSON.parse(firstContent.text);
149 | } catch (parseError) {
150 | console.warn(
151 | "Failed to parse analysis content from memory:",
152 | parseError,
153 | );
154 | return memory.data;
155 | }
156 | }
157 | }
158 |
159 | // Try direct content access (legacy format)
160 | if (memory.content) {
161 | return memory.content;
162 | }
163 |
164 | // Try data field
165 | if (memory.data) {
166 | return memory.data;
167 | }
168 | }
169 | } catch (error) {
170 | console.warn("Failed to retrieve from memory system:", error);
171 | }
172 |
173 | // Fallback to reading from cached analysis file
174 | const analysisPath = path.join(
175 | ".documcp",
176 | "analyses",
177 | `${analysisId}.json`,
178 | );
179 | try {
180 | const content = await fs.readFile(analysisPath, "utf-8");
181 | return JSON.parse(content);
182 | } catch {
183 | throw new Error(
184 | `Repository analysis with ID '${analysisId}' not found. Please run analyze_repository first.`,
185 | );
186 | }
187 | }
188 |
189 | private async loadMemoryInsights(
190 | analysis: any,
191 | options: UpdateOptions,
192 | ): Promise<void> {
193 | try {
194 | // Get similar projects that had successful documentation updates
195 | const similarProjectsQuery = `${
196 | analysis.metadata?.primaryLanguage || ""
197 | } ${analysis.metadata?.ecosystem || ""} documentation update`;
198 | const similarProjects = await handleMemoryRecall({
199 | query: similarProjectsQuery,
200 | type: "recommendation",
201 | limit: 10,
202 | });
203 |
204 | // Get patterns for successful documentation updates
205 | const updatePatternsQuery =
206 | "documentation update successful patterns gaps outdated";
207 | const updatePatterns = await handleMemoryRecall({
208 | query: updatePatternsQuery,
209 | type: "configuration",
210 | limit: 5,
211 | });
212 |
213 | // Get memory-enhanced analysis for this specific update task
214 | const enhancedAnalysis = await handleMemoryIntelligentAnalysis({
215 | projectPath: analysis.projectPath || "",
216 | baseAnalysis: analysis,
217 | });
218 |
219 | // Get memory-enhanced recommendations for update strategy
220 | const enhancedRecommendations = await handleMemoryEnhancedRecommendation({
221 | projectPath: analysis.projectPath || "",
222 | baseRecommendation: {
223 | updateStrategy: options.updateStrategy,
224 | compareMode: options.compareMode,
225 | focusAreas: options.focusAreas || [],
226 | },
227 | projectFeatures: {
228 | ecosystem: analysis.metadata?.ecosystem || "unknown",
229 | primaryLanguage: analysis.metadata?.primaryLanguage || "unknown",
230 | complexity: analysis.complexity || "medium",
231 | hasTests: analysis.structure?.hasTests || false,
232 | hasCI: analysis.structure?.hasCI || false,
233 | docStructure: "existing", // Indicates we're updating existing docs
234 | },
235 | });
236 |
237 | this.memoryInsights = {
238 | similarProjects: similarProjects.memories || [],
239 | updatePatterns: updatePatterns.memories || [],
240 | enhancedAnalysis: enhancedAnalysis,
241 | enhancedRecommendations: enhancedRecommendations,
242 | successfulUpdatePatterns: this.extractUpdatePatterns(
243 | similarProjects.memories || [],
244 | ),
245 | commonGapTypes: this.extractCommonGapTypes(
246 | similarProjects.memories || [],
247 | ),
248 | };
249 | } catch (error) {
250 | console.warn("Failed to load memory insights:", error);
251 | this.memoryInsights = {
252 | similarProjects: [],
253 | updatePatterns: [],
254 | enhancedAnalysis: null,
255 | enhancedRecommendations: null,
256 | successfulUpdatePatterns: [],
257 | commonGapTypes: {},
258 | };
259 | }
260 | }
261 |
262 | private extractUpdatePatterns(projects: any[]): any[] {
263 | return projects
264 | .filter(
265 | (p) => p.content?.updatePatterns || p.content?.documentationUpdates,
266 | )
267 | .map((p) => p.content?.updatePatterns || p.content?.documentationUpdates)
268 | .flat()
269 | .filter(Boolean);
270 | }
271 |
272 | private extractCommonGapTypes(projects: any[]): Record<string, number> {
273 | const gapTypes: Record<string, number> = {};
274 |
275 | projects.forEach((p) => {
276 | const gaps = p.content?.documentationGaps || [];
277 | gaps.forEach((gap: any) => {
278 | const type = gap.type || "unknown";
279 | gapTypes[type] = (gapTypes[type] || 0) + 1;
280 | });
281 | });
282 |
283 | return gapTypes;
284 | }
285 |
286 | private async analyzeExistingDocumentation(
287 | docsPath: string,
288 | ): Promise<Map<string, any>> {
289 | const docs = new Map<string, any>();
290 |
291 | try {
292 | await this.recursivelyAnalyzeDocuments(docsPath, docs);
293 | } catch (error) {
294 | console.warn("Failed to analyze existing documentation:", error);
295 | }
296 |
297 | return docs;
298 | }
299 |
300 | private async recursivelyAnalyzeDocuments(
301 | dirPath: string,
302 | docs: Map<string, any>,
303 | relativePath: string = "",
304 | ): Promise<void> {
305 | try {
306 | const entries = await fs.readdir(dirPath, { withFileTypes: true });
307 |
308 | for (const entry of entries) {
309 | const fullPath = path.join(dirPath, entry.name);
310 | const docPath = path.join(relativePath, entry.name);
311 |
312 | if (entry.isDirectory()) {
313 | await this.recursivelyAnalyzeDocuments(fullPath, docs, docPath);
314 | } else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
315 | try {
316 | const content = await fs.readFile(fullPath, "utf-8");
317 | const analysis = this.analyzeDocumentContent(content, docPath);
318 | docs.set(docPath, {
319 | content,
320 | analysis,
321 | lastModified: (await fs.stat(fullPath)).mtime,
322 | path: fullPath,
323 | });
324 | } catch (error) {
325 | console.warn(`Failed to read document ${fullPath}:`, error);
326 | }
327 | }
328 | }
329 | } catch (error) {
330 | console.warn(`Failed to read directory ${dirPath}:`, error);
331 | }
332 | }
333 |
334 | private analyzeDocumentContent(content: string, filePath: string): any {
335 | return {
336 | type: this.inferDocumentType(filePath, content),
337 | sections: this.extractSections(content),
338 | codeBlocks: this.extractCodeBlocks(content),
339 | links: this.extractLinks(content),
340 | lastUpdated: this.extractLastUpdated(content),
341 | version: this.extractVersion(content),
342 | dependencies: this.extractMentionedDependencies(content),
343 | features: this.extractDocumentedFeatures(content),
344 | wordCount: content.split(/\s+/).length,
345 | headingStructure: this.extractHeadingStructure(content),
346 | };
347 | }
348 |
349 | private inferDocumentType(filePath: string, content: string): string {
350 | const fileName = path.basename(filePath).toLowerCase();
351 | const pathParts = filePath.toLowerCase().split(path.sep);
352 |
353 | // Diataxis categories
354 | if (pathParts.includes("tutorials")) return "tutorial";
355 | if (pathParts.includes("how-to") || pathParts.includes("howto"))
356 | return "how-to";
357 | if (pathParts.includes("reference")) return "reference";
358 | if (pathParts.includes("explanation")) return "explanation";
359 |
360 | // Common documentation types
361 | if (fileName.includes("readme")) return "readme";
362 | if (fileName.includes("getting-started") || fileName.includes("quickstart"))
363 | return "getting-started";
364 | if (fileName.includes("api")) return "api-reference";
365 | if (fileName.includes("install") || fileName.includes("setup"))
366 | return "installation";
367 | if (fileName.includes("deploy")) return "deployment";
368 | if (fileName.includes("config")) return "configuration";
369 |
370 | // Infer from content
371 | if (
372 | content.includes("# Getting Started") ||
373 | content.includes("## Getting Started")
374 | )
375 | return "getting-started";
376 | if (content.includes("# API") || content.includes("## API"))
377 | return "api-reference";
378 | if (
379 | content.includes("# Installation") ||
380 | content.includes("## Installation")
381 | )
382 | return "installation";
383 |
384 | return "general";
385 | }
386 |
387 | private extractSections(content: string): any[] {
388 | const sections: any[] = [];
389 | const lines = content.split("\n");
390 | let currentSection: any = null;
391 |
392 | for (let i = 0; i < lines.length; i++) {
393 | const line = lines[i];
394 | const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
395 |
396 | if (headingMatch) {
397 | if (currentSection) {
398 | sections.push(currentSection);
399 | }
400 |
401 | currentSection = {
402 | level: headingMatch[1].length,
403 | title: headingMatch[2],
404 | startLine: i + 1,
405 | content: [],
406 | };
407 | } else if (currentSection) {
408 | currentSection.content.push(line);
409 | }
410 | }
411 |
412 | if (currentSection) {
413 | sections.push(currentSection);
414 | }
415 |
416 | return sections.map((section) => ({
417 | ...section,
418 | content: section.content.join("\n"),
419 | wordCount: section.content.join(" ").split(/\s+/).length,
420 | }));
421 | }
422 |
423 | private extractCodeBlocks(content: string): any[] {
424 | const codeBlocks: any[] = [];
425 | const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
426 | let match;
427 |
428 | while ((match = codeBlockRegex.exec(content)) !== null) {
429 | codeBlocks.push({
430 | language: match[1] || "text",
431 | code: match[2],
432 | startIndex: match.index,
433 | endIndex: match.index + match[0].length,
434 | });
435 | }
436 |
437 | return codeBlocks;
438 | }
439 |
440 | private extractLinks(content: string): any[] {
441 | const links: any[] = [];
442 | const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
443 | let match;
444 |
445 | while ((match = linkRegex.exec(content)) !== null) {
446 | links.push({
447 | text: match[1],
448 | url: match[2],
449 | isInternal: !match[2].startsWith("http"),
450 | startIndex: match.index,
451 | });
452 | }
453 |
454 | return links;
455 | }
456 |
457 | private extractLastUpdated(content: string): string | null {
458 | const updateMatch = content.match(
459 | /(?:last updated|updated|modified):\s*(.+)/i,
460 | );
461 | return updateMatch ? updateMatch[1] : null;
462 | }
463 |
464 | private extractVersion(content: string): string | null {
465 | const versionMatch = content.match(/(?:version|v)[\s:]+([\d.]+)/i);
466 | return versionMatch ? versionMatch[1] : null;
467 | }
468 |
469 | private extractMentionedDependencies(content: string): string[] {
470 | const dependencies: Set<string> = new Set();
471 |
472 | // Extract from npm install commands
473 | const npmMatches = content.match(/npm install\s+([^`\n]+)/g);
474 | if (npmMatches) {
475 | npmMatches.forEach((match) => {
476 | const packages = match.replace("npm install", "").trim().split(/\s+/);
477 | packages.forEach((pkg) => {
478 | if (pkg && !pkg.startsWith("-")) {
479 | dependencies.add(pkg);
480 | }
481 | });
482 | });
483 | }
484 |
485 | // Extract from import statements
486 | const importMatches = content.match(/import.*from\s+['"]([^'"]+)['"]/g);
487 | if (importMatches) {
488 | importMatches.forEach((match) => {
489 | const packageMatch = match.match(/from\s+['"]([^'"]+)['"]/);
490 | if (packageMatch && !packageMatch[1].startsWith(".")) {
491 | dependencies.add(packageMatch[1]);
492 | }
493 | });
494 | }
495 |
496 | return Array.from(dependencies);
497 | }
498 |
499 | private extractDocumentedFeatures(content: string): string[] {
500 | const features: Set<string> = new Set();
501 |
502 | // Extract function names from code blocks
503 | const functionMatches = content.match(
504 | /(?:function|const|let|var)\s+(\w+)/g,
505 | );
506 | if (functionMatches) {
507 | functionMatches.forEach((match) => {
508 | const functionMatch = match.match(/(?:function|const|let|var)\s+(\w+)/);
509 | if (functionMatch) {
510 | features.add(functionMatch[1]);
511 | }
512 | });
513 | }
514 |
515 | // Extract API endpoints
516 | const apiMatches = content.match(
517 | /(?:GET|POST|PUT|DELETE|PATCH)\s+([/\w-]+)/g,
518 | );
519 | if (apiMatches) {
520 | apiMatches.forEach((match) => {
521 | const endpointMatch = match.match(
522 | /(?:GET|POST|PUT|DELETE|PATCH)\s+([/\w-]+)/,
523 | );
524 | if (endpointMatch) {
525 | features.add(endpointMatch[1]);
526 | }
527 | });
528 | }
529 |
530 | // Extract mentioned features from headings
531 | const headings = content.match(/#{1,6}\s+(.+)/g);
532 | if (headings) {
533 | headings.forEach((heading) => {
534 | const headingText = heading.replace(/#{1,6}\s+/, "").toLowerCase();
535 | if (
536 | headingText.includes("feature") ||
537 | headingText.includes("functionality")
538 | ) {
539 | features.add(headingText);
540 | }
541 | });
542 | }
543 |
544 | return Array.from(features);
545 | }
546 |
547 | private extractHeadingStructure(content: string): any[] {
548 | const headings: any[] = [];
549 | const lines = content.split("\n");
550 |
551 | lines.forEach((line, index) => {
552 | const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
553 | if (headingMatch) {
554 | headings.push({
555 | level: headingMatch[1].length,
556 | text: headingMatch[2],
557 | line: index + 1,
558 | });
559 | }
560 | });
561 |
562 | return headings;
563 | }
564 |
565 | private async performCodeDocumentationComparison(
566 | analysis: any,
567 | existingDocs: Map<string, any>,
568 | _options: UpdateOptions,
569 | ): Promise<CodeDocumentationComparison> {
570 | const codeFeatures = this.extractCodeFeatures(analysis);
571 | const documentedFeatures = this.extractAllDocumentedFeatures(existingDocs);
572 |
573 | const gaps = await this.detectDocumentationGaps(
574 | codeFeatures,
575 | documentedFeatures,
576 | _options,
577 | );
578 | const outdatedSections = await this.detectOutdatedSections(
579 | analysis,
580 | existingDocs,
581 | );
582 | const accuracyIssues = await this.detectAccuracyIssues(
583 | analysis,
584 | existingDocs,
585 | );
586 |
587 | return {
588 | codeFeatures,
589 | documentedFeatures,
590 | gaps,
591 | outdatedSections,
592 | accuracyIssues,
593 | };
594 | }
595 |
596 | private extractCodeFeatures(analysis: any): any[] {
597 | const features: any[] = [];
598 |
599 | // Extract from dependencies
600 | if (analysis.dependencies?.packages) {
601 | analysis.dependencies.packages.forEach((pkg: string) => {
602 | features.push({
603 | type: "dependency",
604 | name: pkg,
605 | source: "package.json",
606 | });
607 | });
608 | }
609 |
610 | // Extract from scripts
611 | const packageJson = this.findPackageJsonInAnalysis(analysis);
612 | if (packageJson?.scripts) {
613 | Object.keys(packageJson.scripts).forEach((script) => {
614 | features.push({
615 | type: "script",
616 | name: script,
617 | command: packageJson.scripts[script],
618 | source: "package.json",
619 | });
620 | });
621 | }
622 |
623 | // Extract from file structure
624 | if (analysis.structure) {
625 | if (analysis.structure.hasTests) {
626 | features.push({
627 | type: "testing",
628 | name: "test suite",
629 | source: "structure",
630 | });
631 | }
632 | if (analysis.structure.hasCI) {
633 | features.push({
634 | type: "ci-cd",
635 | name: "continuous integration",
636 | source: "structure",
637 | });
638 | }
639 | }
640 |
641 | // Extract from technologies
642 | if (analysis.technologies) {
643 | Object.entries(analysis.technologies).forEach(([key, value]) => {
644 | if (value) {
645 | features.push({
646 | type: "technology",
647 | name: key,
648 | value: value,
649 | source: "analysis",
650 | });
651 | }
652 | });
653 | }
654 |
655 | return features;
656 | }
657 |
658 | private findPackageJsonInAnalysis(analysis: any): any {
659 | const files = analysis.files || [];
660 | const packageFile = files.find((f: any) => f.name === "package.json");
661 |
662 | if (packageFile?.content) {
663 | try {
664 | return JSON.parse(packageFile.content);
665 | } catch {
666 | return null;
667 | }
668 | }
669 |
670 | return null;
671 | }
672 |
673 | private extractAllDocumentedFeatures(existingDocs: Map<string, any>): any[] {
674 | const allFeatures: any[] = [];
675 |
676 | existingDocs.forEach((doc, docPath) => {
677 | const features = doc.analysis?.features || [];
678 | const dependencies = doc.analysis?.dependencies || [];
679 |
680 | features.forEach((feature: string) => {
681 | allFeatures.push({
682 | name: feature,
683 | source: docPath,
684 | type: "documented-feature",
685 | });
686 | });
687 |
688 | dependencies.forEach((dep: string) => {
689 | allFeatures.push({
690 | name: dep,
691 | source: docPath,
692 | type: "documented-dependency",
693 | });
694 | });
695 | });
696 |
697 | return allFeatures;
698 | }
699 |
700 | private async detectDocumentationGaps(
701 | codeFeatures: any[],
702 | documentedFeatures: any[],
703 | _options: UpdateOptions,
704 | ): Promise<DocumentationGap[]> {
705 | const gaps: DocumentationGap[] = [];
706 | const memoryGapPatterns = this.memoryInsights?.commonGapTypes || {};
707 |
708 | // Find features in code that aren't documented
709 | codeFeatures.forEach((codeFeature) => {
710 | const isDocumented = documentedFeatures.some((docFeature) =>
711 | this.featuresMatch(codeFeature, docFeature),
712 | );
713 |
714 | if (!isDocumented) {
715 | const severity = this.determineGapSeverity(
716 | codeFeature,
717 | memoryGapPatterns,
718 | );
719 | const suggestedUpdate = this.generateGapSuggestion(
720 | codeFeature,
721 | _options,
722 | );
723 |
724 | gaps.push({
725 | type: "missing",
726 | location: `${codeFeature.source} -> documentation`,
727 | description: `${codeFeature.type} '${codeFeature.name}' exists in code but is not documented`,
728 | severity,
729 | suggestedUpdate,
730 | memoryEvidence: this.findMemoryEvidenceForGap(codeFeature),
731 | });
732 | }
733 | });
734 |
735 | // Find documented features that no longer exist in code
736 | documentedFeatures.forEach((docFeature) => {
737 | const existsInCode = codeFeatures.some((codeFeature) =>
738 | this.featuresMatch(codeFeature, docFeature),
739 | );
740 |
741 | if (!existsInCode) {
742 | gaps.push({
743 | type: "outdated",
744 | location: docFeature.source,
745 | description: `Documented feature '${docFeature.name}' no longer exists in code`,
746 | severity: "medium",
747 | suggestedUpdate: `Remove or update documentation for '${docFeature.name}'`,
748 | memoryEvidence: this.findMemoryEvidenceForOutdated(docFeature),
749 | });
750 | }
751 | });
752 |
753 | return gaps;
754 | }
755 |
756 | private featuresMatch(codeFeature: any, docFeature: any): boolean {
757 | // Exact name match
758 | if (codeFeature.name === docFeature.name) return true;
759 |
760 | // Type-specific matching
761 | if (
762 | codeFeature.type === "dependency" &&
763 | docFeature.type === "documented-dependency"
764 | ) {
765 | return codeFeature.name === docFeature.name;
766 | }
767 |
768 | // Partial match for similar names
769 | const codeName = codeFeature.name.toLowerCase();
770 | const docName = docFeature.name.toLowerCase();
771 |
772 | return codeName.includes(docName) || docName.includes(codeName);
773 | }
774 |
775 | private determineGapSeverity(
776 | codeFeature: any,
777 | memoryGapPatterns: Record<string, number>,
778 | ): "low" | "medium" | "high" | "critical" {
779 | // High importance features
780 | if (
781 | codeFeature.type === "script" &&
782 | ["start", "dev", "build", "test"].includes(codeFeature.name)
783 | ) {
784 | return "high";
785 | }
786 |
787 | if (
788 | codeFeature.type === "dependency" &&
789 | this.isCriticalDependency(codeFeature.name)
790 | ) {
791 | return "high";
792 | }
793 |
794 | if (codeFeature.type === "testing" || codeFeature.type === "ci-cd") {
795 | return "medium";
796 | }
797 |
798 | // Check memory patterns for common gaps
799 | const gapFrequency = memoryGapPatterns[codeFeature.type] || 0;
800 | if (gapFrequency > 5) return "medium"; // Common gap type
801 | if (gapFrequency > 2) return "low";
802 |
803 | return "low";
804 | }
805 |
806 | private isCriticalDependency(depName: string): boolean {
807 | const criticalDeps = [
808 | "react",
809 | "vue",
810 | "angular",
811 | "express",
812 | "fastify",
813 | "next",
814 | "nuxt",
815 | "gatsby",
816 | "typescript",
817 | "jest",
818 | "mocha",
819 | "webpack",
820 | "vite",
821 | "rollup",
822 | ];
823 |
824 | return criticalDeps.some((critical) => depName.includes(critical));
825 | }
826 |
827 | private generateGapSuggestion(
828 | codeFeature: any,
829 | _options: UpdateOptions,
830 | ): string {
831 | switch (codeFeature.type) {
832 | case "script":
833 | return `Add documentation for the '${codeFeature.name}' script: \`npm run ${codeFeature.name}\``;
834 | case "dependency":
835 | return `Document the '${codeFeature.name}' dependency and its usage`;
836 | case "testing":
837 | return `Add testing documentation explaining how to run and write tests`;
838 | case "ci-cd":
839 | return `Document the CI/CD pipeline and deployment process`;
840 | case "technology":
841 | return `Add explanation for ${codeFeature.name}: ${codeFeature.value}`;
842 | default:
843 | return `Document the ${codeFeature.type} '${codeFeature.name}'`;
844 | }
845 | }
846 |
847 | private findMemoryEvidenceForGap(codeFeature: any): any[] {
848 | return (
849 | this.memoryInsights?.similarProjects
850 | .filter(
851 | (p: any) =>
852 | p.content?.gaps?.some((gap: any) => gap.type === codeFeature.type),
853 | )
854 | .slice(0, 3) || []
855 | );
856 | }
857 |
858 | private findMemoryEvidenceForOutdated(docFeature: any): any[] {
859 | return (
860 | this.memoryInsights?.similarProjects
861 | .filter(
862 | (p: any) =>
863 | p.content?.outdatedSections?.some(
864 | (section: any) => section.feature === docFeature.name,
865 | ),
866 | )
867 | .slice(0, 3) || []
868 | );
869 | }
870 |
871 | private async detectOutdatedSections(
872 | analysis: any,
873 | existingDocs: Map<string, any>,
874 | ): Promise<any[]> {
875 | const outdatedSections: any[] = [];
876 |
877 | existingDocs.forEach((doc, docPath) => {
878 | const sections = doc.analysis?.sections || [];
879 |
880 | sections.forEach((section: any) => {
881 | const isOutdated = this.checkSectionOutdated(section, analysis);
882 |
883 | if (isOutdated) {
884 | outdatedSections.push({
885 | location: docPath,
886 | section: section.title,
887 | reason: isOutdated.reason,
888 | confidence: isOutdated.confidence,
889 | suggestedUpdate: isOutdated.suggestedUpdate,
890 | });
891 | }
892 | });
893 | });
894 |
895 | return outdatedSections;
896 | }
897 |
898 | private checkSectionOutdated(section: any, analysis: any): any {
899 | const sectionContent = section.content.toLowerCase();
900 |
901 | // Check for outdated Node.js versions
902 | const nodeVersionMatch = sectionContent.match(/node(?:\.js)?\s+(\d+)/);
903 | if (nodeVersionMatch) {
904 | const documentedVersion = parseInt(nodeVersionMatch[1], 10);
905 | const currentRecommended = 18; // Current LTS
906 |
907 | if (documentedVersion < currentRecommended - 2) {
908 | return {
909 | reason: `Documented Node.js version ${documentedVersion} is outdated`,
910 | confidence: 0.9,
911 | suggestedUpdate: `Update to recommend Node.js ${currentRecommended}+`,
912 | };
913 | }
914 | }
915 |
916 | // Check for outdated package names
917 | const packageJson = this.findPackageJsonInAnalysis(analysis);
918 | if (packageJson?.dependencies) {
919 | const currentDeps = Object.keys(packageJson.dependencies);
920 |
921 | // Look for documented packages that are no longer dependencies
922 | for (const dep of currentDeps) {
923 | if (sectionContent.includes(dep)) {
924 | const version = packageJson.dependencies[dep];
925 | if (
926 | sectionContent.includes(dep) &&
927 | !sectionContent.includes(version)
928 | ) {
929 | return {
930 | reason: `Package version information may be outdated for ${dep}`,
931 | confidence: 0.7,
932 | suggestedUpdate: `Update ${dep} version references to ${version}`,
933 | };
934 | }
935 | }
936 | }
937 | }
938 |
939 | return null;
940 | }
941 |
942 | private async detectAccuracyIssues(
943 | analysis: any,
944 | existingDocs: Map<string, any>,
945 | ): Promise<any[]> {
946 | const accuracyIssues: any[] = [];
947 |
948 | existingDocs.forEach((doc, docPath) => {
949 | const codeBlocks = doc.analysis?.codeBlocks || [];
950 |
951 | codeBlocks.forEach((codeBlock: any, index: number) => {
952 | const issues = this.validateCodeBlock(codeBlock, analysis);
953 |
954 | issues.forEach((issue) => {
955 | accuracyIssues.push({
956 | location: `${docPath}:code-block-${index}`,
957 | type: issue.type,
958 | description: issue.description,
959 | severity: issue.severity,
960 | suggestedFix: issue.suggestedFix,
961 | });
962 | });
963 | });
964 | });
965 |
966 | return accuracyIssues;
967 | }
968 |
969 | private validateCodeBlock(codeBlock: any, analysis: any): any[] {
970 | const issues: any[] = [];
971 | const code = codeBlock.code;
972 |
973 | // Check npm install commands against actual dependencies
974 | const npmInstallMatches = code.match(/npm install\s+([^`\n]+)/g);
975 | if (npmInstallMatches) {
976 | const packageJson = this.findPackageJsonInAnalysis(analysis);
977 | const actualDeps = packageJson
978 | ? Object.keys(packageJson.dependencies || {})
979 | : [];
980 |
981 | npmInstallMatches.forEach((match: string) => {
982 | const packages = match.replace("npm install", "").trim().split(/\s+/);
983 | packages.forEach((pkg: string) => {
984 | if (pkg && !pkg.startsWith("-") && !actualDeps.includes(pkg)) {
985 | issues.push({
986 | type: "incorrect-dependency",
987 | description: `npm install command includes '${pkg}' which is not in package.json`,
988 | severity: "medium",
989 | suggestedFix: `Remove '${pkg}' or add it to dependencies`,
990 | });
991 | }
992 | });
993 | });
994 | }
995 |
996 | // Check for outdated import syntax
997 | if (
998 | code.includes("require(") &&
999 | analysis.metadata?.primaryLanguage === "TypeScript"
1000 | ) {
1001 | issues.push({
1002 | type: "outdated-syntax",
1003 | description: "Using require() syntax in TypeScript project",
1004 | severity: "low",
1005 | suggestedFix: "Update to ES6 import syntax",
1006 | });
1007 | }
1008 |
1009 | return issues;
1010 | }
1011 |
1012 | private async generateUpdateRecommendations(
1013 | comparison: CodeDocumentationComparison,
1014 | _options: UpdateOptions,
1015 | ): Promise<UpdateRecommendation[]> {
1016 | const recommendations: UpdateRecommendation[] = [];
1017 |
1018 | // Generate recommendations for gaps
1019 | for (const gap of comparison.gaps) {
1020 | if (
1021 | gap.severity === "critical" ||
1022 | gap.severity === "high" ||
1023 | (gap.severity === "medium" &&
1024 | _options.updateStrategy !== "conservative")
1025 | ) {
1026 | const recommendation = await this.generateGapRecommendation(
1027 | gap,
1028 | _options,
1029 | );
1030 | recommendations.push(recommendation);
1031 | }
1032 | }
1033 |
1034 | // Generate recommendations for outdated sections
1035 | for (const outdated of comparison.outdatedSections) {
1036 | const recommendation = await this.generateOutdatedRecommendation(
1037 | outdated,
1038 | _options,
1039 | );
1040 | recommendations.push(recommendation);
1041 | }
1042 |
1043 | // Generate recommendations for accuracy issues
1044 | for (const issue of comparison.accuracyIssues) {
1045 | if (
1046 | issue.severity !== "low" ||
1047 | _options.updateStrategy === "aggressive"
1048 | ) {
1049 | const recommendation = await this.generateAccuracyRecommendation(
1050 | issue,
1051 | _options,
1052 | );
1053 | recommendations.push(recommendation);
1054 | }
1055 | }
1056 |
1057 | return recommendations.sort((a, b) => b.confidence - a.confidence);
1058 | }
1059 |
1060 | private async generateGapRecommendation(
1061 | gap: DocumentationGap,
1062 | _options: UpdateOptions,
1063 | ): Promise<UpdateRecommendation> {
1064 | const memoryEvidence = gap.memoryEvidence || [];
1065 | const successfulPatterns =
1066 | this.memoryInsights?.successfulUpdatePatterns || [];
1067 |
1068 | return {
1069 | section: gap.location,
1070 | currentContent: "", // No current content for missing items
1071 | suggestedContent: this.generateContentForGap(gap, successfulPatterns),
1072 | reasoning: `${gap.description}. ${memoryEvidence.length} similar projects had similar gaps.`,
1073 | memoryEvidence,
1074 | confidence: this.calculateGapConfidence(gap, memoryEvidence),
1075 | effort: this.estimateGapEffort(gap),
1076 | };
1077 | }
1078 |
1079 | private generateContentForGap(
1080 | gap: DocumentationGap,
1081 | patterns: any[],
1082 | ): string {
1083 | // Use memory patterns to generate appropriate content
1084 | const relevantPatterns = patterns.filter((p) => p.gapType === gap.type);
1085 |
1086 | if (relevantPatterns.length > 0) {
1087 | const bestPattern = relevantPatterns[0];
1088 | return this.adaptPatternToGap(bestPattern, gap);
1089 | }
1090 |
1091 | return gap.suggestedUpdate;
1092 | }
1093 |
1094 | private adaptPatternToGap(pattern: any, gap: DocumentationGap): string {
1095 | let content = pattern.template || pattern.content || gap.suggestedUpdate;
1096 |
1097 | // Replace placeholders with actual gap information
1098 | content = content.replace(/\{feature\}/g, gap.description);
1099 | content = content.replace(/\{location\}/g, gap.location);
1100 |
1101 | return content;
1102 | }
1103 |
1104 | private calculateGapConfidence(
1105 | gap: DocumentationGap,
1106 | evidence: any[],
1107 | ): number {
1108 | let confidence = 0.5; // Base confidence
1109 |
1110 | // Increase confidence based on severity
1111 | switch (gap.severity) {
1112 | case "critical":
1113 | confidence += 0.4;
1114 | break;
1115 | case "high":
1116 | confidence += 0.3;
1117 | break;
1118 | case "medium":
1119 | confidence += 0.2;
1120 | break;
1121 | case "low":
1122 | confidence += 0.1;
1123 | break;
1124 | }
1125 |
1126 | // Increase confidence based on memory evidence
1127 | confidence += Math.min(evidence.length * 0.1, 0.3);
1128 |
1129 | return Math.min(confidence, 1.0);
1130 | }
1131 |
1132 | private estimateGapEffort(gap: DocumentationGap): "low" | "medium" | "high" {
1133 | switch (gap.type) {
1134 | case "missing":
1135 | return gap.severity === "critical" ? "high" : "medium";
1136 | case "outdated":
1137 | return "low";
1138 | case "incorrect":
1139 | return "medium";
1140 | case "incomplete":
1141 | return "low";
1142 | default:
1143 | return "medium";
1144 | }
1145 | }
1146 |
1147 | private async generateOutdatedRecommendation(
1148 | outdated: any,
1149 | _options: UpdateOptions,
1150 | ): Promise<UpdateRecommendation> {
1151 | return {
1152 | section: outdated.location,
1153 | currentContent: outdated.section,
1154 | suggestedContent: outdated.suggestedUpdate,
1155 | reasoning: outdated.reason,
1156 | memoryEvidence: [],
1157 | confidence: outdated.confidence || 0.8,
1158 | effort: "low",
1159 | };
1160 | }
1161 |
1162 | private async generateAccuracyRecommendation(
1163 | issue: any,
1164 | _options: UpdateOptions,
1165 | ): Promise<UpdateRecommendation> {
1166 | return {
1167 | section: issue.location,
1168 | currentContent: "Code block with accuracy issues",
1169 | suggestedContent: issue.suggestedFix,
1170 | reasoning: issue.description,
1171 | memoryEvidence: [],
1172 | confidence: issue.severity === "high" ? 0.9 : 0.7,
1173 | effort: issue.severity === "high" ? "medium" : "low",
1174 | };
1175 | }
1176 |
1177 | private calculateUpdateMetrics(
1178 | comparison: CodeDocumentationComparison,
1179 | recommendations: UpdateRecommendation[],
1180 | ): any {
1181 | const totalGaps = comparison.gaps.length;
1182 | const totalRecommendations = recommendations.length;
1183 | const avgConfidence =
1184 | recommendations.reduce((sum, r) => sum + r.confidence, 0) /
1185 | recommendations.length || 0;
1186 |
1187 | const effortCounts = recommendations.reduce(
1188 | (acc, r) => {
1189 | acc[r.effort] = (acc[r.effort] || 0) + 1;
1190 | return acc;
1191 | },
1192 | {} as Record<string, number>,
1193 | );
1194 |
1195 | let estimatedEffort = "low";
1196 | if (effortCounts.high > 0) estimatedEffort = "high";
1197 | else if (effortCounts.medium > effortCounts.low) estimatedEffort = "medium";
1198 |
1199 | return {
1200 | gapsDetected: totalGaps,
1201 | recommendationsGenerated: totalRecommendations,
1202 | confidenceScore: Math.round(avgConfidence * 100) / 100,
1203 | estimatedEffort,
1204 | };
1205 | }
1206 |
1207 | private generateMemoryInformedNextSteps(
1208 | comparison: CodeDocumentationComparison,
1209 | recommendations: UpdateRecommendation[],
1210 | ): string[] {
1211 | const nextSteps = [];
1212 | const highConfidenceRecs = recommendations.filter(
1213 | (r) => r.confidence > 0.8,
1214 | );
1215 | const criticalGaps = comparison.gaps.filter(
1216 | (g) => g.severity === "critical",
1217 | );
1218 |
1219 | if (criticalGaps.length > 0) {
1220 | nextSteps.push(
1221 | `Address ${criticalGaps.length} critical documentation gaps immediately`,
1222 | );
1223 | }
1224 |
1225 | if (highConfidenceRecs.length > 0) {
1226 | nextSteps.push(
1227 | `Implement ${highConfidenceRecs.length} high-confidence recommendations first`,
1228 | );
1229 | }
1230 |
1231 | if (comparison.accuracyIssues.length > 0) {
1232 | nextSteps.push(
1233 | `Fix ${comparison.accuracyIssues.length} code accuracy issues in documentation`,
1234 | );
1235 | }
1236 |
1237 | nextSteps.push(
1238 | "Review and validate all recommended changes before implementation",
1239 | );
1240 | nextSteps.push("Test updated code examples to ensure they work correctly");
1241 |
1242 | const memoryInsights = this.memoryInsights?.similarProjects?.length || 0;
1243 | if (memoryInsights > 0) {
1244 | nextSteps.push(
1245 | `Leverage patterns from ${memoryInsights} similar projects for additional improvements`,
1246 | );
1247 | }
1248 |
1249 | return nextSteps;
1250 | }
1251 | }
1252 |
1253 | // Export the tool implementation
1254 | export const updateExistingDocumentation: Tool = {
1255 | name: "update_existing_documentation",
1256 | description:
1257 | "Intelligently analyze and update existing documentation using memory insights and code comparison",
1258 | inputSchema: {
1259 | type: "object",
1260 | properties: {
1261 | analysisId: {
1262 | type: "string",
1263 | description: "Repository analysis ID from analyze_repository tool",
1264 | },
1265 | docsPath: {
1266 | type: "string",
1267 | description: "Path to existing documentation directory",
1268 | },
1269 | compareMode: {
1270 | type: "string",
1271 | enum: ["comprehensive", "gap-detection", "accuracy-check"],
1272 | default: "comprehensive",
1273 | description: "Mode of comparison between code and documentation",
1274 | },
1275 | updateStrategy: {
1276 | type: "string",
1277 | enum: ["conservative", "moderate", "aggressive"],
1278 | default: "moderate",
1279 | description: "How aggressively to suggest updates",
1280 | },
1281 | preserveStyle: {
1282 | type: "boolean",
1283 | default: true,
1284 | description: "Preserve existing documentation style and formatting",
1285 | },
1286 | focusAreas: {
1287 | type: "array",
1288 | items: { type: "string" },
1289 | description:
1290 | 'Specific areas to focus updates on (e.g., "dependencies", "scripts", "api")',
1291 | },
1292 | },
1293 | required: ["analysisId", "docsPath"],
1294 | },
1295 | };
1296 |
1297 | export async function handleUpdateExistingDocumentation(
1298 | args: any,
1299 | ): Promise<UpdateResult> {
1300 | const engine = new DocumentationUpdateEngine();
1301 | return await engine.updateExistingDocumentation(args);
1302 | }
1303 |
```
--------------------------------------------------------------------------------
/src/memory/visualization.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Memory Visualization Interface for DocuMCP
3 | * Generate visual representations of memory data, patterns, and insights
4 | */
5 |
6 | import { EventEmitter } from "events";
7 | import { MemoryEntry, JSONLStorage } from "./storage.js";
8 | import { MemoryManager } from "./manager.js";
9 | import { IncrementalLearningSystem } from "./learning.js";
10 | import { KnowledgeGraph } from "./knowledge-graph.js";
11 | import { TemporalMemoryAnalysis } from "./temporal-analysis.js";
12 |
13 | export interface VisualizationConfig {
14 | width: number;
15 | height: number;
16 | theme: "light" | "dark" | "auto";
17 | colorScheme: string[];
18 | interactive: boolean;
19 | exportFormat: "svg" | "png" | "json" | "html";
20 | responsive: boolean;
21 | }
22 |
23 | export interface ChartData {
24 | type:
25 | | "line"
26 | | "bar"
27 | | "scatter"
28 | | "heatmap"
29 | | "network"
30 | | "sankey"
31 | | "treemap"
32 | | "timeline";
33 | title: string;
34 | description: string;
35 | data: any;
36 | config: Partial<VisualizationConfig>;
37 | metadata: {
38 | generated: Date;
39 | dataPoints: number;
40 | timeRange?: { start: Date; end: Date };
41 | filters?: Record<string, any>;
42 | };
43 | }
44 |
45 | export interface DashboardData {
46 | title: string;
47 | description: string;
48 | charts: ChartData[];
49 | summary: {
50 | totalEntries: number;
51 | timeRange: { start: Date; end: Date };
52 | keyInsights: string[];
53 | healthScore: number;
54 | };
55 | generated: Date;
56 | }
57 |
58 | export interface NetworkVisualization {
59 | nodes: Array<{
60 | id: string;
61 | label: string;
62 | group: string;
63 | size: number;
64 | color: string;
65 | metadata: any;
66 | }>;
67 | edges: Array<{
68 | source: string;
69 | target: string;
70 | weight: number;
71 | type: string;
72 | color: string;
73 | metadata: any;
74 | }>;
75 | layout: "force" | "circular" | "hierarchical" | "grid";
76 | clustering: boolean;
77 | }
78 |
79 | export interface HeatmapVisualization {
80 | data: number[][];
81 | labels: {
82 | x: string[];
83 | y: string[];
84 | };
85 | colorScale: {
86 | min: number;
87 | max: number;
88 | colors: string[];
89 | };
90 | title: string;
91 | description: string;
92 | }
93 |
94 | export interface TimelineVisualization {
95 | events: Array<{
96 | id: string;
97 | timestamp: Date;
98 | title: string;
99 | description: string;
100 | type: string;
101 | importance: number;
102 | color: string;
103 | metadata: any;
104 | }>;
105 | timeRange: { start: Date; end: Date };
106 | granularity: "hour" | "day" | "week" | "month";
107 | groupBy?: string;
108 | }
109 |
110 | export class MemoryVisualizationSystem extends EventEmitter {
111 | private storage: JSONLStorage;
112 | private manager: MemoryManager;
113 | private learningSystem: IncrementalLearningSystem;
114 | private knowledgeGraph: KnowledgeGraph;
115 | private temporalAnalysis: TemporalMemoryAnalysis;
116 | private defaultConfig: VisualizationConfig;
117 | private visualizationCache: Map<string, ChartData>;
118 |
119 | constructor(
120 | storage: JSONLStorage,
121 | manager: MemoryManager,
122 | learningSystem: IncrementalLearningSystem,
123 | knowledgeGraph: KnowledgeGraph,
124 | temporalAnalysis: TemporalMemoryAnalysis,
125 | ) {
126 | super();
127 | this.storage = storage;
128 | this.manager = manager;
129 | this.learningSystem = learningSystem;
130 | this.knowledgeGraph = knowledgeGraph;
131 | this.temporalAnalysis = temporalAnalysis;
132 | this.visualizationCache = new Map();
133 |
134 | this.defaultConfig = {
135 | width: 800,
136 | height: 600,
137 | theme: "light",
138 | colorScheme: [
139 | "#3B82F6", // Blue
140 | "#10B981", // Green
141 | "#F59E0B", // Yellow
142 | "#EF4444", // Red
143 | "#8B5CF6", // Purple
144 | "#06B6D4", // Cyan
145 | "#F97316", // Orange
146 | "#84CC16", // Lime
147 | ],
148 | interactive: true,
149 | exportFormat: "svg",
150 | responsive: true,
151 | };
152 | }
153 |
154 | /**
155 | * Generate comprehensive dashboard
156 | */
157 | async generateDashboard(options?: {
158 | timeRange?: { start: Date; end: Date };
159 | includeCharts?: string[];
160 | config?: Partial<VisualizationConfig>;
161 | }): Promise<DashboardData> {
162 | const timeRange = options?.timeRange || this.getDefaultTimeRange();
163 | const config = { ...this.defaultConfig, ...options?.config };
164 |
165 | this.emit("dashboard_generation_started", { timeRange });
166 |
167 | try {
168 | const charts: ChartData[] = [];
169 |
170 | // Activity Timeline
171 | if (
172 | !options?.includeCharts ||
173 | options.includeCharts.includes("activity")
174 | ) {
175 | charts.push(await this.generateActivityTimeline(timeRange, config));
176 | }
177 |
178 | // Memory Type Distribution
179 | if (
180 | !options?.includeCharts ||
181 | options.includeCharts.includes("distribution")
182 | ) {
183 | charts.push(
184 | await this.generateMemoryTypeDistribution(timeRange, config),
185 | );
186 | }
187 |
188 | // Success Rate Trends
189 | if (
190 | !options?.includeCharts ||
191 | options.includeCharts.includes("success")
192 | ) {
193 | charts.push(await this.generateSuccessRateTrends(timeRange, config));
194 | }
195 |
196 | // Knowledge Graph Network
197 | if (
198 | !options?.includeCharts ||
199 | options.includeCharts.includes("network")
200 | ) {
201 | charts.push(await this.generateKnowledgeGraphVisualization(config));
202 | }
203 |
204 | // Learning Patterns Heatmap
205 | if (
206 | !options?.includeCharts ||
207 | options.includeCharts.includes("learning")
208 | ) {
209 | charts.push(await this.generateLearningPatternsHeatmap(config));
210 | }
211 |
212 | // Temporal Patterns
213 | if (
214 | !options?.includeCharts ||
215 | options.includeCharts.includes("temporal")
216 | ) {
217 | charts.push(
218 | await this.generateTemporalPatternsChart(timeRange, config),
219 | );
220 | }
221 |
222 | // Project Correlation Matrix
223 | if (
224 | !options?.includeCharts ||
225 | options.includeCharts.includes("correlation")
226 | ) {
227 | charts.push(
228 | await this.generateProjectCorrelationMatrix(timeRange, config),
229 | );
230 | }
231 |
232 | // Get summary data
233 | const entries = await this.getEntriesInTimeRange(timeRange);
234 | const keyInsights = await this.generateKeyInsights(entries, timeRange);
235 | const healthScore = await this.calculateSystemHealthScore(entries);
236 |
237 | const dashboard: DashboardData = {
238 | title: "DocuMCP Memory System Dashboard",
239 | description: `Comprehensive overview of memory system activity from ${timeRange.start.toLocaleDateString()} to ${timeRange.end.toLocaleDateString()}`,
240 | charts,
241 | summary: {
242 | totalEntries: entries.length,
243 | timeRange,
244 | keyInsights,
245 | healthScore,
246 | },
247 | generated: new Date(),
248 | };
249 |
250 | this.emit("dashboard_generated", {
251 | charts: charts.length,
252 | entries: entries.length,
253 | timeRange,
254 | });
255 |
256 | return dashboard;
257 | } catch (error) {
258 | this.emit("dashboard_error", {
259 | error: error instanceof Error ? error.message : String(error),
260 | });
261 | throw error;
262 | }
263 | }
264 |
265 | /**
266 | * Generate activity timeline chart
267 | */
268 | async generateActivityTimeline(
269 | timeRange: { start: Date; end: Date },
270 | config: Partial<VisualizationConfig>,
271 | ): Promise<ChartData> {
272 | const entries = await this.getEntriesInTimeRange(timeRange);
273 |
274 | // Group entries by day
275 | const dailyData = new Map<string, number>();
276 | const successData = new Map<string, number>();
277 |
278 | for (const entry of entries) {
279 | const day = entry.timestamp.slice(0, 10); // YYYY-MM-DD
280 | dailyData.set(day, (dailyData.get(day) || 0) + 1);
281 |
282 | if (entry.data.outcome === "success" || entry.data.success === true) {
283 | successData.set(day, (successData.get(day) || 0) + 1);
284 | }
285 | }
286 |
287 | // Create time series data
288 | const datasets = [
289 | {
290 | label: "Total Activity",
291 | data: Array.from(dailyData.entries()).map(([date, count]) => ({
292 | x: date,
293 | y: count,
294 | })),
295 | borderColor: config.colorScheme?.[0] || "#3B82F6",
296 | backgroundColor: config.colorScheme?.[0] || "#3B82F6",
297 | fill: false,
298 | },
299 | {
300 | label: "Successful Activities",
301 | data: Array.from(successData.entries()).map(([date, count]) => ({
302 | x: date,
303 | y: count,
304 | })),
305 | borderColor: config.colorScheme?.[1] || "#10B981",
306 | backgroundColor: config.colorScheme?.[1] || "#10B981",
307 | fill: false,
308 | },
309 | ];
310 |
311 | return {
312 | type: "line",
313 | title: "Memory Activity Timeline",
314 | description:
315 | "Daily memory system activity showing total entries and successful outcomes",
316 | data: {
317 | datasets,
318 | options: {
319 | responsive: config.responsive,
320 | plugins: {
321 | title: {
322 | display: true,
323 | text: "Memory Activity Over Time",
324 | },
325 | legend: {
326 | display: true,
327 | position: "top",
328 | },
329 | },
330 | scales: {
331 | x: {
332 | type: "time",
333 | time: {
334 | unit: "day",
335 | },
336 | title: {
337 | display: true,
338 | text: "Date",
339 | },
340 | },
341 | y: {
342 | title: {
343 | display: true,
344 | text: "Number of Entries",
345 | },
346 | },
347 | },
348 | },
349 | },
350 | config,
351 | metadata: {
352 | generated: new Date(),
353 | dataPoints: entries.length,
354 | timeRange,
355 | filters: { type: "activity_timeline" },
356 | },
357 | };
358 | }
359 |
360 | /**
361 | * Generate memory type distribution chart
362 | */
363 | async generateMemoryTypeDistribution(
364 | timeRange: { start: Date; end: Date },
365 | config: Partial<VisualizationConfig>,
366 | ): Promise<ChartData> {
367 | const entries = await this.getEntriesInTimeRange(timeRange);
368 |
369 | // Count entries by type
370 | const typeCounts = new Map<string, number>();
371 | for (const entry of entries) {
372 | typeCounts.set(entry.type, (typeCounts.get(entry.type) || 0) + 1);
373 | }
374 |
375 | // Sort by count
376 | const sortedTypes = Array.from(typeCounts.entries()).sort(
377 | ([, a], [, b]) => b - a,
378 | );
379 |
380 | const data = {
381 | labels: sortedTypes.map(([type]) => type),
382 | datasets: [
383 | {
384 | data: sortedTypes.map(([, count]) => count),
385 | backgroundColor: config.colorScheme || this.defaultConfig.colorScheme,
386 | borderColor:
387 | config.colorScheme?.map((c) => this.darkenColor(c)) ||
388 | this.defaultConfig.colorScheme.map((c) => this.darkenColor(c)),
389 | borderWidth: 2,
390 | },
391 | ],
392 | };
393 |
394 | return {
395 | type: "bar",
396 | title: "Memory Type Distribution",
397 | description: "Distribution of memory entries by type",
398 | data: {
399 | ...data,
400 | options: {
401 | responsive: config.responsive,
402 | plugins: {
403 | title: {
404 | display: true,
405 | text: "Memory Entry Types",
406 | },
407 | legend: {
408 | display: false,
409 | },
410 | },
411 | scales: {
412 | y: {
413 | beginAtZero: true,
414 | title: {
415 | display: true,
416 | text: "Number of Entries",
417 | },
418 | },
419 | x: {
420 | title: {
421 | display: true,
422 | text: "Memory Type",
423 | },
424 | },
425 | },
426 | },
427 | },
428 | config,
429 | metadata: {
430 | generated: new Date(),
431 | dataPoints: entries.length,
432 | timeRange,
433 | filters: { type: "type_distribution" },
434 | },
435 | };
436 | }
437 |
438 | /**
439 | * Generate success rate trends chart
440 | */
441 | async generateSuccessRateTrends(
442 | timeRange: { start: Date; end: Date },
443 | config: Partial<VisualizationConfig>,
444 | ): Promise<ChartData> {
445 | const entries = await this.getEntriesInTimeRange(timeRange);
446 |
447 | // Group by week and calculate success rates
448 | const weeklyData = new Map<string, { total: number; successful: number }>();
449 |
450 | for (const entry of entries) {
451 | const week = this.getWeekKey(new Date(entry.timestamp));
452 | const current = weeklyData.get(week) || { total: 0, successful: 0 };
453 |
454 | current.total++;
455 | if (entry.data.outcome === "success" || entry.data.success === true) {
456 | current.successful++;
457 | }
458 |
459 | weeklyData.set(week, current);
460 | }
461 |
462 | // Calculate success rates
463 | const data = Array.from(weeklyData.entries())
464 | .map(([week, stats]) => ({
465 | x: week,
466 | y: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0,
467 | total: stats.total,
468 | successful: stats.successful,
469 | }))
470 | .sort((a, b) => a.x.localeCompare(b.x));
471 |
472 | return {
473 | type: "line",
474 | title: "Success Rate Trends",
475 | description: "Weekly success rate trends for memory system operations",
476 | data: {
477 | datasets: [
478 | {
479 | label: "Success Rate (%)",
480 | data: data,
481 | borderColor: config.colorScheme?.[1] || "#10B981",
482 | backgroundColor: config.colorScheme?.[1] || "#10B981",
483 | fill: false,
484 | tension: 0.1,
485 | },
486 | ],
487 | options: {
488 | responsive: config.responsive,
489 | plugins: {
490 | title: {
491 | display: true,
492 | text: "Success Rate Over Time",
493 | },
494 | tooltip: {
495 | callbacks: {
496 | afterBody: (context: any) => {
497 | const point = data[context[0].dataIndex];
498 | return `Total: ${point.total}, Successful: ${point.successful}`;
499 | },
500 | },
501 | },
502 | },
503 | scales: {
504 | x: {
505 | title: {
506 | display: true,
507 | text: "Week",
508 | },
509 | },
510 | y: {
511 | beginAtZero: true,
512 | max: 100,
513 | title: {
514 | display: true,
515 | text: "Success Rate (%)",
516 | },
517 | },
518 | },
519 | },
520 | },
521 | config,
522 | metadata: {
523 | generated: new Date(),
524 | dataPoints: data.length,
525 | timeRange,
526 | filters: { type: "success_trends" },
527 | },
528 | };
529 | }
530 |
531 | /**
532 | * Generate knowledge graph network visualization
533 | */
534 | async generateKnowledgeGraphVisualization(
535 | config: Partial<VisualizationConfig>,
536 | ): Promise<ChartData> {
537 | const allNodes = await this.knowledgeGraph.getAllNodes();
538 | const allEdges = await this.knowledgeGraph.getAllEdges();
539 |
540 | // Prepare network data
541 | const networkData: NetworkVisualization = {
542 | nodes: allNodes.map((node) => ({
543 | id: node.id,
544 | label: node.label || node.id.slice(0, 10),
545 | group: node.type,
546 | size: Math.max(10, Math.min(30, (node.weight || 1) * 10)),
547 | color: this.getColorForNodeType(node.type, config.colorScheme),
548 | metadata: node.properties,
549 | })),
550 | edges: allEdges.map((edge) => ({
551 | source: edge.source,
552 | target: edge.target,
553 | weight: edge.weight,
554 | type: edge.type,
555 | color: this.getColorForEdgeType(edge.type, config.colorScheme),
556 | metadata: edge.properties,
557 | })),
558 | layout: "force",
559 | clustering: true,
560 | };
561 |
562 | return {
563 | type: "network",
564 | title: "Knowledge Graph Network",
565 | description:
566 | "Interactive network visualization of memory relationships and connections",
567 | data: networkData,
568 | config,
569 | metadata: {
570 | generated: new Date(),
571 | dataPoints: allNodes.length + allEdges.length,
572 | filters: { type: "knowledge_graph" },
573 | },
574 | };
575 | }
576 |
577 | /**
578 | * Generate learning patterns heatmap
579 | */
580 | async generateLearningPatternsHeatmap(
581 | config: Partial<VisualizationConfig>,
582 | ): Promise<ChartData> {
583 | const patterns = await this.learningSystem.getPatterns();
584 |
585 | // Create correlation matrix between different pattern dimensions
586 | const frameworks = [
587 | ...new Set(
588 | patterns
589 | .flatMap((p) => p.metadata.technologies || [])
590 | .filter(
591 | (t) =>
592 | t.includes("framework") ||
593 | t.includes("js") ||
594 | t.includes("react") ||
595 | t.includes("vue"),
596 | ),
597 | ),
598 | ];
599 | const languages = [
600 | ...new Set(
601 | patterns
602 | .flatMap((p) => p.metadata.technologies || [])
603 | .filter((t) => !t.includes("framework")),
604 | ),
605 | ];
606 |
607 | const heatmapData: number[][] = [];
608 | const labels = { x: frameworks, y: languages };
609 |
610 | for (const language of languages) {
611 | const row: number[] = [];
612 | for (const framework of frameworks) {
613 | // Calculate correlation/co-occurrence
614 | const langPatterns = patterns.filter(
615 | (p) => p.metadata.technologies?.includes(language),
616 | );
617 | const frameworkPatterns = patterns.filter(
618 | (p) => p.metadata.technologies?.includes(framework),
619 | );
620 | const bothPatterns = patterns.filter(
621 | (p) =>
622 | p.metadata.technologies?.includes(language) &&
623 | p.metadata.technologies?.includes(framework),
624 | );
625 |
626 | const correlation =
627 | langPatterns.length > 0 && frameworkPatterns.length > 0
628 | ? bothPatterns.length /
629 | Math.min(langPatterns.length, frameworkPatterns.length)
630 | : 0;
631 |
632 | row.push(correlation);
633 | }
634 | heatmapData.push(row);
635 | }
636 |
637 | const heatmap: HeatmapVisualization = {
638 | data: heatmapData,
639 | labels,
640 | colorScale: {
641 | min: 0,
642 | max: 1,
643 | colors: ["#F3F4F6", "#93C5FD", "#3B82F6", "#1D4ED8", "#1E3A8A"],
644 | },
645 | title: "Language-Framework Learning Patterns",
646 | description:
647 | "Correlation matrix showing relationships between programming languages and frameworks in learning patterns",
648 | };
649 |
650 | return {
651 | type: "heatmap",
652 | title: "Learning Patterns Heatmap",
653 | description:
654 | "Visualization of learning pattern correlations across languages and frameworks",
655 | data: heatmap,
656 | config,
657 | metadata: {
658 | generated: new Date(),
659 | dataPoints: patterns.length,
660 | filters: { type: "learning_patterns" },
661 | },
662 | };
663 | }
664 |
665 | /**
666 | * Generate temporal patterns chart
667 | */
668 | async generateTemporalPatternsChart(
669 | timeRange: { start: Date; end: Date },
670 | config: Partial<VisualizationConfig>,
671 | ): Promise<ChartData> {
672 | const patterns = await this.temporalAnalysis.analyzeTemporalPatterns({
673 | granularity: "day",
674 | aggregation: "count",
675 | timeRange: {
676 | start: timeRange.start,
677 | end: timeRange.end,
678 | duration: timeRange.end.getTime() - timeRange.start.getTime(),
679 | label: "Analysis Period",
680 | },
681 | });
682 |
683 | // Prepare data for different pattern types
684 | const patternData = patterns.map((pattern) => ({
685 | type: pattern.type,
686 | confidence: pattern.confidence,
687 | description: pattern.description,
688 | dataPoints: pattern.dataPoints?.length || 0,
689 | }));
690 |
691 | const data = {
692 | labels: patternData.map((p) => p.type),
693 | datasets: [
694 | {
695 | label: "Pattern Confidence",
696 | data: patternData.map((p) => p.confidence * 100),
697 | backgroundColor: config.colorScheme || this.defaultConfig.colorScheme,
698 | borderColor:
699 | config.colorScheme?.map((c) => this.darkenColor(c)) ||
700 | this.defaultConfig.colorScheme.map((c) => this.darkenColor(c)),
701 | borderWidth: 2,
702 | },
703 | ],
704 | };
705 |
706 | return {
707 | type: "bar",
708 | title: "Temporal Patterns Analysis",
709 | description:
710 | "Confidence levels of detected temporal patterns in memory activity",
711 | data: {
712 | ...data,
713 | options: {
714 | responsive: config.responsive,
715 | plugins: {
716 | title: {
717 | display: true,
718 | text: "Detected Temporal Patterns",
719 | },
720 | tooltip: {
721 | callbacks: {
722 | afterBody: (context: any) => {
723 | const pattern = patternData[context[0].dataIndex];
724 | return pattern.description;
725 | },
726 | },
727 | },
728 | },
729 | scales: {
730 | y: {
731 | beginAtZero: true,
732 | max: 100,
733 | title: {
734 | display: true,
735 | text: "Confidence (%)",
736 | },
737 | },
738 | x: {
739 | title: {
740 | display: true,
741 | text: "Pattern Type",
742 | },
743 | },
744 | },
745 | },
746 | },
747 | config,
748 | metadata: {
749 | generated: new Date(),
750 | dataPoints: patterns.length,
751 | timeRange,
752 | filters: { type: "temporal_patterns" },
753 | },
754 | };
755 | }
756 |
757 | /**
758 | * Generate project correlation matrix
759 | */
760 | async generateProjectCorrelationMatrix(
761 | timeRange: { start: Date; end: Date },
762 | config: Partial<VisualizationConfig>,
763 | ): Promise<ChartData> {
764 | const entries = await this.getEntriesInTimeRange(timeRange);
765 |
766 | // Extract unique projects
767 | const projects = [
768 | ...new Set(
769 | entries
770 | .map((e) => e.data.projectPath || e.data.projectId || "Unknown")
771 | .filter((p) => p !== "Unknown"),
772 | ),
773 | ].slice(0, 10); // Limit to top 10
774 |
775 | // Calculate correlation matrix
776 | const correlationMatrix: number[][] = [];
777 |
778 | for (const project1 of projects) {
779 | const row: number[] = [];
780 | for (const project2 of projects) {
781 | if (project1 === project2) {
782 | row.push(1.0);
783 | } else {
784 | const correlation = this.calculateProjectCorrelation(
785 | entries,
786 | project1,
787 | project2,
788 | );
789 | row.push(correlation);
790 | }
791 | }
792 | correlationMatrix.push(row);
793 | }
794 |
795 | const heatmap: HeatmapVisualization = {
796 | data: correlationMatrix,
797 | labels: { x: projects, y: projects },
798 | colorScale: {
799 | min: -1,
800 | max: 1,
801 | colors: ["#EF4444", "#F59E0B", "#F3F4F6", "#10B981", "#059669"],
802 | },
803 | title: "Project Correlation Matrix",
804 | description:
805 | "Correlation matrix showing relationships between different projects based on memory patterns",
806 | };
807 |
808 | return {
809 | type: "heatmap",
810 | title: "Project Correlations",
811 | description:
812 | "Visualization of correlations between different projects in the memory system",
813 | data: heatmap,
814 | config,
815 | metadata: {
816 | generated: new Date(),
817 | dataPoints: projects.length * projects.length,
818 | timeRange,
819 | filters: { type: "project_correlation" },
820 | },
821 | };
822 | }
823 |
824 | /**
825 | * Generate custom visualization
826 | */
827 | async generateCustomVisualization(
828 | type: ChartData["type"],
829 | query: {
830 | filters?: Record<string, any>;
831 | timeRange?: { start: Date; end: Date };
832 | aggregation?: string;
833 | groupBy?: string;
834 | },
835 | config?: Partial<VisualizationConfig>,
836 | ): Promise<ChartData> {
837 | const activeConfig = { ...this.defaultConfig, ...config };
838 | const timeRange = query.timeRange || this.getDefaultTimeRange();
839 |
840 | let entries = await this.getEntriesInTimeRange(timeRange);
841 |
842 | // Apply filters
843 | if (query.filters) {
844 | entries = this.applyFilters(entries, query.filters);
845 | }
846 |
847 | switch (type) {
848 | case "timeline":
849 | return this.generateTimelineVisualization(entries, query, activeConfig);
850 | case "scatter":
851 | return this.generateScatterPlot(entries, query, activeConfig);
852 | case "treemap":
853 | return this.generateTreemapVisualization(entries, query, activeConfig);
854 | case "sankey":
855 | return this.generateSankeyDiagram(entries, query, activeConfig);
856 | default:
857 | throw new Error(`Unsupported visualization type: ${type}`);
858 | }
859 | }
860 |
861 | /**
862 | * Export visualization to specified format
863 | */
864 | async exportVisualization(
865 | chartData: ChartData,
866 | format: "svg" | "png" | "json" | "html" = "json",
867 | options?: {
868 | filename?: string;
869 | quality?: number;
870 | width?: number;
871 | height?: number;
872 | },
873 | ): Promise<string | Buffer> {
874 | this.emit("export_started", { type: chartData.type, format });
875 |
876 | try {
877 | switch (format) {
878 | case "json":
879 | return JSON.stringify(chartData, null, 2);
880 |
881 | case "html":
882 | return this.generateHTMLVisualization(chartData, options);
883 |
884 | case "svg":
885 | return this.generateSVGVisualization(chartData, options);
886 |
887 | case "png":
888 | // This would require a rendering library like Puppeteer
889 | throw new Error(
890 | "PNG export requires additional rendering capabilities",
891 | );
892 |
893 | default:
894 | throw new Error(`Unsupported export format: ${format}`);
895 | }
896 | } catch (error) {
897 | this.emit("export_error", {
898 | error: error instanceof Error ? error.message : String(error),
899 | });
900 | throw error;
901 | }
902 | }
903 |
904 | /**
905 | * Helper methods
906 | */
907 | private async getEntriesInTimeRange(timeRange: {
908 | start: Date;
909 | end: Date;
910 | }): Promise<MemoryEntry[]> {
911 | const allEntries = await this.storage.getAll();
912 | return allEntries.filter((entry) => {
913 | const entryDate = new Date(entry.timestamp);
914 | return entryDate >= timeRange.start && entryDate <= timeRange.end;
915 | });
916 | }
917 |
918 | private getDefaultTimeRange(): { start: Date; end: Date } {
919 | const end = new Date();
920 | const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
921 | return { start, end };
922 | }
923 |
924 | private getWeekKey(date: Date): string {
925 | const year = date.getFullYear();
926 | const week = this.getWeekNumber(date);
927 | return `${year}-W${week.toString().padStart(2, "0")}`;
928 | }
929 |
930 | private getWeekNumber(date: Date): number {
931 | const d = new Date(
932 | Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
933 | );
934 | const dayNum = d.getUTCDay() || 7;
935 | d.setUTCDate(d.getUTCDate() + 4 - dayNum);
936 | const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
937 | return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
938 | }
939 |
940 | private getColorForNodeType(type: string, colorScheme?: string[]): string {
941 | const colors = colorScheme || this.defaultConfig.colorScheme;
942 | const index = type.charCodeAt(0) % colors.length;
943 | return colors[index];
944 | }
945 |
946 | private getColorForEdgeType(type: string, colorScheme?: string[]): string {
947 | const colors = colorScheme || this.defaultConfig.colorScheme;
948 | const typeColors: Record<string, string> = {
949 | similarity: colors[0],
950 | dependency: colors[1],
951 | temporal: colors[2],
952 | causal: colors[3],
953 | };
954 | return typeColors[type] || colors[4];
955 | }
956 |
957 | private darkenColor(color: string): string {
958 | // Simple color darkening - in production, use a proper color library
959 | if (color.startsWith("#")) {
960 | const hex = color.slice(1);
961 | const num = parseInt(hex, 16);
962 | const r = Math.max(0, (num >> 16) - 40);
963 | const g = Math.max(0, ((num >> 8) & 0x00ff) - 40);
964 | const b = Math.max(0, (num & 0x0000ff) - 40);
965 | return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
966 | }
967 | return color;
968 | }
969 |
970 | private calculateProjectCorrelation(
971 | entries: MemoryEntry[],
972 | project1: string,
973 | project2: string,
974 | ): number {
975 | const entries1 = entries.filter(
976 | (e) =>
977 | e.data.projectPath?.includes(project1) || e.data.projectId === project1,
978 | );
979 | const entries2 = entries.filter(
980 | (e) =>
981 | e.data.projectPath?.includes(project2) || e.data.projectId === project2,
982 | );
983 |
984 | if (entries1.length === 0 || entries2.length === 0) return 0;
985 |
986 | // Simple correlation based on shared characteristics
987 | let sharedFeatures = 0;
988 | let totalFeatures = 0;
989 |
990 | // Compare languages
991 | const lang1 = new Set(entries1.map((e) => e.data.language).filter(Boolean));
992 | const lang2 = new Set(entries2.map((e) => e.data.language).filter(Boolean));
993 | const sharedLangs = new Set([...lang1].filter((l) => lang2.has(l)));
994 | sharedFeatures += sharedLangs.size;
995 | totalFeatures += new Set([...lang1, ...lang2]).size;
996 |
997 | // Compare frameworks
998 | const fw1 = new Set(entries1.map((e) => e.data.framework).filter(Boolean));
999 | const fw2 = new Set(entries2.map((e) => e.data.framework).filter(Boolean));
1000 | const sharedFws = new Set([...fw1].filter((f) => fw2.has(f)));
1001 | sharedFeatures += sharedFws.size;
1002 | totalFeatures += new Set([...fw1, ...fw2]).size;
1003 |
1004 | return totalFeatures > 0 ? sharedFeatures / totalFeatures : 0;
1005 | }
1006 |
1007 | private applyFilters(
1008 | entries: MemoryEntry[],
1009 | filters: Record<string, any>,
1010 | ): MemoryEntry[] {
1011 | return entries.filter((entry) => {
1012 | for (const [key, value] of Object.entries(filters)) {
1013 | switch (key) {
1014 | case "type":
1015 | if (Array.isArray(value) && !value.includes(entry.type))
1016 | return false;
1017 | if (typeof value === "string" && entry.type !== value) return false;
1018 | break;
1019 | case "outcome":
1020 | if (entry.data.outcome !== value) return false;
1021 | break;
1022 | case "language":
1023 | if (entry.data.language !== value) return false;
1024 | break;
1025 | case "framework":
1026 | if (entry.data.framework !== value) return false;
1027 | break;
1028 | case "project":
1029 | if (
1030 | !entry.data.projectPath?.includes(value) &&
1031 | entry.data.projectId !== value
1032 | ) {
1033 | return false;
1034 | }
1035 | break;
1036 | case "tags":
1037 | if (
1038 | Array.isArray(value) &&
1039 | !value.some((tag) => entry.tags?.includes(tag))
1040 | ) {
1041 | return false;
1042 | }
1043 | break;
1044 | }
1045 | }
1046 | return true;
1047 | });
1048 | }
1049 |
1050 | private async generateKeyInsights(
1051 | entries: MemoryEntry[],
1052 | timeRange: { start: Date; end: Date },
1053 | ): Promise<string[]> {
1054 | const insights: string[] = [];
1055 |
1056 | // Activity insight
1057 | const dailyAverage =
1058 | entries.length /
1059 | Math.max(
1060 | 1,
1061 | Math.ceil(
1062 | (timeRange.end.getTime() - timeRange.start.getTime()) /
1063 | (24 * 60 * 60 * 1000),
1064 | ),
1065 | );
1066 | insights.push(`Average ${dailyAverage.toFixed(1)} entries per day`);
1067 |
1068 | // Success rate insight
1069 | const successful = entries.filter(
1070 | (e) => e.data.outcome === "success" || e.data.success === true,
1071 | ).length;
1072 | const successRate =
1073 | entries.length > 0 ? (successful / entries.length) * 100 : 0;
1074 | insights.push(`${successRate.toFixed(1)}% success rate`);
1075 |
1076 | // Most common type
1077 | const typeCounts = new Map<string, number>();
1078 | entries.forEach((e) =>
1079 | typeCounts.set(e.type, (typeCounts.get(e.type) || 0) + 1),
1080 | );
1081 | const mostCommonType = Array.from(typeCounts.entries()).sort(
1082 | ([, a], [, b]) => b - a,
1083 | )[0];
1084 | if (mostCommonType) {
1085 | insights.push(
1086 | `Most common activity: ${mostCommonType[0]} (${mostCommonType[1]} entries)`,
1087 | );
1088 | }
1089 |
1090 | // Growth trend
1091 | const midpoint = new Date(
1092 | (timeRange.start.getTime() + timeRange.end.getTime()) / 2,
1093 | );
1094 | const firstHalf = entries.filter(
1095 | (e) => new Date(e.timestamp) < midpoint,
1096 | ).length;
1097 | const secondHalf = entries.filter(
1098 | (e) => new Date(e.timestamp) >= midpoint,
1099 | ).length;
1100 | if (firstHalf > 0) {
1101 | const growthRate = ((secondHalf - firstHalf) / firstHalf) * 100;
1102 | insights.push(
1103 | `Activity ${growthRate >= 0 ? "increased" : "decreased"} by ${Math.abs(
1104 | growthRate,
1105 | ).toFixed(1)}%`,
1106 | );
1107 | }
1108 |
1109 | return insights.slice(0, 5); // Return top 5 insights
1110 | }
1111 |
1112 | private async calculateSystemHealthScore(
1113 | entries: MemoryEntry[],
1114 | ): Promise<number> {
1115 | let score = 0;
1116 |
1117 | // Activity level (0-25 points)
1118 | const recentEntries = entries.filter(
1119 | (e) =>
1120 | new Date(e.timestamp) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
1121 | );
1122 | score += Math.min(25, recentEntries.length * 2);
1123 |
1124 | // Success rate (0-25 points)
1125 | const successful = entries.filter(
1126 | (e) => e.data.outcome === "success" || e.data.success === true,
1127 | ).length;
1128 | const successRate = entries.length > 0 ? successful / entries.length : 0;
1129 | score += successRate * 25;
1130 |
1131 | // Diversity (0-25 points)
1132 | const uniqueTypes = new Set(entries.map((e) => e.type)).size;
1133 | score += Math.min(25, uniqueTypes * 3);
1134 |
1135 | // Consistency (0-25 points)
1136 | if (entries.length >= 7) {
1137 | const dailyActivities = new Map<string, number>();
1138 | entries.forEach((e) => {
1139 | const day = e.timestamp.slice(0, 10);
1140 | dailyActivities.set(day, (dailyActivities.get(day) || 0) + 1);
1141 | });
1142 |
1143 | const values = Array.from(dailyActivities.values());
1144 | const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
1145 | const variance =
1146 | values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
1147 | values.length;
1148 | const consistency =
1149 | mean > 0 ? Math.max(0, 1 - Math.sqrt(variance) / mean) : 0;
1150 | score += consistency * 25;
1151 | }
1152 |
1153 | return Math.round(Math.min(100, score));
1154 | }
1155 |
1156 | private generateTimelineVisualization(
1157 | entries: MemoryEntry[],
1158 | query: any,
1159 | config: VisualizationConfig,
1160 | ): ChartData {
1161 | const events = entries.map((entry) => ({
1162 | id: entry.id,
1163 | timestamp: new Date(entry.timestamp),
1164 | title: entry.type,
1165 | description: entry.data.description || `${entry.type} entry`,
1166 | type: entry.type,
1167 | importance: entry.data.outcome === "success" ? 1 : 0.5,
1168 | color: this.getColorForNodeType(entry.type, config.colorScheme),
1169 | metadata: entry.data,
1170 | }));
1171 |
1172 | const timelineData: TimelineVisualization = {
1173 | events,
1174 | timeRange: {
1175 | start: new Date(Math.min(...events.map((e) => e.timestamp.getTime()))),
1176 | end: new Date(Math.max(...events.map((e) => e.timestamp.getTime()))),
1177 | },
1178 | granularity: "day",
1179 | groupBy: query.groupBy,
1180 | };
1181 |
1182 | return {
1183 | type: "timeline",
1184 | title: "Memory Activity Timeline",
1185 | description: "Chronological timeline of memory system activities",
1186 | data: timelineData,
1187 | config,
1188 | metadata: {
1189 | generated: new Date(),
1190 | dataPoints: events.length,
1191 | filters: query.filters,
1192 | },
1193 | };
1194 | }
1195 |
1196 | private generateScatterPlot(
1197 | entries: MemoryEntry[],
1198 | query: any,
1199 | config: VisualizationConfig,
1200 | ): ChartData {
1201 | // Create scatter plot data based on timestamp vs some metric
1202 | const data = entries.map((entry) => ({
1203 | x: new Date(entry.timestamp).getTime(),
1204 | y: entry.data.duration || entry.data.complexity || Math.random(), // Use available metric
1205 | color: this.getColorForNodeType(entry.type, config.colorScheme),
1206 | metadata: entry,
1207 | }));
1208 |
1209 | return {
1210 | type: "scatter",
1211 | title: "Memory Activity Scatter Plot",
1212 | description: "Scatter plot visualization of memory activities",
1213 | data: {
1214 | datasets: [
1215 | {
1216 | label: "Activities",
1217 | data: data,
1218 | backgroundColor: data.map((d) => d.color),
1219 | },
1220 | ],
1221 | },
1222 | config,
1223 | metadata: {
1224 | generated: new Date(),
1225 | dataPoints: data.length,
1226 | filters: query.filters,
1227 | },
1228 | };
1229 | }
1230 |
1231 | private generateTreemapVisualization(
1232 | entries: MemoryEntry[],
1233 | query: any,
1234 | config: VisualizationConfig,
1235 | ): ChartData {
1236 | // Group entries by type and project for treemap
1237 | const hierarchy = new Map<string, Map<string, number>>();
1238 |
1239 | for (const entry of entries) {
1240 | const type = entry.type;
1241 | const project =
1242 | entry.data.projectPath || entry.data.projectId || "Unknown";
1243 |
1244 | if (!hierarchy.has(type)) {
1245 | hierarchy.set(type, new Map());
1246 | }
1247 | hierarchy
1248 | .get(type)!
1249 | .set(project, (hierarchy.get(type)!.get(project) || 0) + 1);
1250 | }
1251 |
1252 | // Convert to treemap format
1253 | const treemapData = Array.from(hierarchy.entries()).map(
1254 | ([type, projects]) => ({
1255 | name: type,
1256 | value: Array.from(projects.values()).reduce((sum, val) => sum + val, 0),
1257 | children: Array.from(projects.entries()).map(([project, count]) => ({
1258 | name: project,
1259 | value: count,
1260 | })),
1261 | }),
1262 | );
1263 |
1264 | return {
1265 | type: "treemap",
1266 | title: "Memory Type Hierarchy",
1267 | description: "Hierarchical treemap of memory entries by type and project",
1268 | data: treemapData,
1269 | config,
1270 | metadata: {
1271 | generated: new Date(),
1272 | dataPoints: entries.length,
1273 | filters: query.filters,
1274 | },
1275 | };
1276 | }
1277 |
1278 | private generateSankeyDiagram(
1279 | entries: MemoryEntry[],
1280 | query: any,
1281 | config: VisualizationConfig,
1282 | ): ChartData {
1283 | // Create flow data from entry types to outcomes
1284 | const flows = new Map<string, Map<string, number>>();
1285 |
1286 | for (const entry of entries) {
1287 | const source = entry.type;
1288 | const target =
1289 | entry.data.outcome || (entry.data.success ? "success" : "unknown");
1290 |
1291 | if (!flows.has(source)) {
1292 | flows.set(source, new Map());
1293 | }
1294 | flows.get(source)!.set(target, (flows.get(source)!.get(target) || 0) + 1);
1295 | }
1296 |
1297 | // Convert to Sankey format
1298 | const nodes: string[] = [];
1299 | const links: Array<{ source: number; target: number; value: number }> = [];
1300 |
1301 | // Collect all unique nodes
1302 | const sources = Array.from(flows.keys());
1303 | const targets = new Set<string>();
1304 | flows.forEach((targetMap) => {
1305 | targetMap.forEach((_, target) => targets.add(target));
1306 | });
1307 |
1308 | nodes.push(
1309 | ...sources,
1310 | ...Array.from(targets).filter((t) => !sources.includes(t)),
1311 | );
1312 |
1313 | // Create links
1314 | flows.forEach((targetMap, source) => {
1315 | targetMap.forEach((value, target) => {
1316 | const sourceIndex = nodes.indexOf(source);
1317 | const targetIndex = nodes.indexOf(target);
1318 | if (sourceIndex !== -1 && targetIndex !== -1) {
1319 | links.push({ source: sourceIndex, target: targetIndex, value });
1320 | }
1321 | });
1322 | });
1323 |
1324 | return {
1325 | type: "sankey",
1326 | title: "Memory Flow Diagram",
1327 | description: "Sankey diagram showing flow from memory types to outcomes",
1328 | data: { nodes, links },
1329 | config,
1330 | metadata: {
1331 | generated: new Date(),
1332 | dataPoints: links.length,
1333 | filters: query.filters,
1334 | },
1335 | };
1336 | }
1337 |
1338 | private generateHTMLVisualization(
1339 | chartData: ChartData,
1340 | _options?: any,
1341 | ): string {
1342 | // Generate basic HTML with embedded Chart.js or D3.js
1343 | return `
1344 | <!DOCTYPE html>
1345 | <html>
1346 | <head>
1347 | <title>${chartData.title}</title>
1348 | <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1349 | <style>
1350 | body { font-family: Arial, sans-serif; margin: 20px; }
1351 | .chart-container { width: 100%; height: 400px; }
1352 | .description { margin-bottom: 20px; color: #666; }
1353 | </style>
1354 | </head>
1355 | <body>
1356 | <h1>${chartData.title}</h1>
1357 | <p class="description">${chartData.description}</p>
1358 | <div class="chart-container">
1359 | <canvas id="chart"></canvas>
1360 | </div>
1361 | <script>
1362 | const ctx = document.getElementById('chart').getContext('2d');
1363 | new Chart(ctx, ${JSON.stringify(chartData.data)});
1364 | </script>
1365 | </body>
1366 | </html>`;
1367 | }
1368 |
1369 | private generateSVGVisualization(
1370 | chartData: ChartData,
1371 | options?: any,
1372 | ): string {
1373 | // Generate basic SVG - in production, use a proper chart library
1374 | const width = options?.width || 800;
1375 | const height = options?.height || 600;
1376 |
1377 | return `
1378 | <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
1379 | <rect width="100%" height="100%" fill="white"/>
1380 | <text x="50%" y="30" text-anchor="middle" font-size="18" font-weight="bold">
1381 | ${chartData.title}
1382 | </text>
1383 | <text x="50%" y="50" text-anchor="middle" font-size="14" fill="#666">
1384 | ${chartData.description}
1385 | </text>
1386 | <!-- Chart data would be rendered here -->
1387 | <text x="50%" y="${
1388 | height / 2
1389 | }" text-anchor="middle" font-size="12" fill="#999">
1390 | Chart visualization (${chartData.metadata.dataPoints} data points)
1391 | </text>
1392 | </svg>`;
1393 | }
1394 | }
1395 |
```
--------------------------------------------------------------------------------
/src/memory/temporal-analysis.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Temporal Memory Analysis System for DocuMCP
3 | * Time-based analysis of memory patterns, trends, and predictions
4 | */
5 |
6 | import { EventEmitter } from "events";
7 | import { MemoryEntry, JSONLStorage } from "./storage.js";
8 | import { MemoryManager } from "./manager.js";
9 | import { IncrementalLearningSystem } from "./learning.js";
10 | import { KnowledgeGraph } from "./knowledge-graph.js";
11 |
12 | export interface TimeWindow {
13 | start: Date;
14 | end: Date;
15 | duration: number; // in milliseconds
16 | label: string;
17 | }
18 |
19 | export interface TemporalPattern {
20 | type: "periodic" | "trending" | "seasonal" | "burst" | "decay";
21 | confidence: number;
22 | period?: number; // For periodic patterns (in milliseconds)
23 | trend?: "increasing" | "decreasing" | "stable";
24 | seasonality?: "daily" | "weekly" | "monthly" | "yearly";
25 | description: string;
26 | dataPoints: Array<{ timestamp: Date; value: number; metadata?: any }>;
27 | }
28 |
29 | export interface TemporalMetrics {
30 | activityLevel: number; // 0-1 scale
31 | growthRate: number; // percentage change
32 | peakActivity: { timestamp: Date; count: number };
33 | averageInterval: number; // average time between entries
34 | consistency: number; // 0-1 scale of temporal consistency
35 | cyclicalStrength: number; // 0-1 scale of cyclical patterns
36 | }
37 |
38 | export interface PredictionResult {
39 | nextActivity: {
40 | probability: number;
41 | timeRange: TimeWindow;
42 | expectedCount: number;
43 | confidence: number;
44 | };
45 | trends: {
46 | shortTerm: TemporalPattern[];
47 | longTerm: TemporalPattern[];
48 | };
49 | anomalies: Array<{
50 | timestamp: Date;
51 | type: "spike" | "drought" | "shift";
52 | severity: number;
53 | description: string;
54 | }>;
55 | recommendations: string[];
56 | }
57 |
58 | export interface TemporalQuery {
59 | timeRange?: TimeWindow;
60 | granularity: "hour" | "day" | "week" | "month" | "year";
61 | aggregation: "count" | "success_rate" | "activity_level" | "diversity";
62 | filters?: {
63 | types?: string[];
64 | projects?: string[];
65 | outcomes?: string[];
66 | tags?: string[];
67 | };
68 | smoothing?: {
69 | enabled: boolean;
70 | method: "moving_average" | "exponential" | "savitzky_golay";
71 | window: number;
72 | };
73 | }
74 |
75 | export interface TemporalInsight {
76 | type: "pattern" | "anomaly" | "trend" | "prediction";
77 | title: string;
78 | description: string;
79 | confidence: number;
80 | timeframe: TimeWindow;
81 | actionable: boolean;
82 | recommendations?: string[];
83 | visualData?: any;
84 | }
85 |
86 | export class TemporalMemoryAnalysis extends EventEmitter {
87 | private storage: JSONLStorage;
88 | private manager: MemoryManager;
89 | private learningSystem: IncrementalLearningSystem;
90 | private knowledgeGraph: KnowledgeGraph;
91 | private patternCache: Map<string, TemporalPattern[]>;
92 | private metricsCache: Map<string, TemporalMetrics>;
93 | private predictionCache: Map<string, PredictionResult>;
94 |
95 | constructor(
96 | storage: JSONLStorage,
97 | manager: MemoryManager,
98 | learningSystem: IncrementalLearningSystem,
99 | knowledgeGraph: KnowledgeGraph,
100 | ) {
101 | super();
102 | this.storage = storage;
103 | this.manager = manager;
104 | this.learningSystem = learningSystem;
105 | this.knowledgeGraph = knowledgeGraph;
106 | this.patternCache = new Map();
107 | this.metricsCache = new Map();
108 | this.predictionCache = new Map();
109 |
110 | this.setupPeriodicAnalysis();
111 | }
112 |
113 | /**
114 | * Analyze temporal patterns in memory data
115 | */
116 | async analyzeTemporalPatterns(
117 | query?: TemporalQuery,
118 | ): Promise<TemporalPattern[]> {
119 | const defaultQuery: TemporalQuery = {
120 | granularity: "day",
121 | aggregation: "count",
122 | timeRange: this.getDefaultTimeRange(),
123 | smoothing: {
124 | enabled: true,
125 | method: "moving_average",
126 | window: 7,
127 | },
128 | };
129 |
130 | const activeQuery = { ...defaultQuery, ...query };
131 | const cacheKey = this.generateCacheKey("patterns", activeQuery);
132 |
133 | // Check cache first
134 | if (this.patternCache.has(cacheKey)) {
135 | return this.patternCache.get(cacheKey)!;
136 | }
137 |
138 | try {
139 | // Get time series data
140 | const timeSeries = await this.buildTimeSeries(activeQuery);
141 |
142 | // Detect different types of patterns
143 | const patterns: TemporalPattern[] = [];
144 |
145 | // Periodic patterns
146 | patterns.push(
147 | ...(await this.detectPeriodicPatterns(timeSeries, activeQuery)),
148 | );
149 |
150 | // Trend patterns
151 | patterns.push(
152 | ...(await this.detectTrendPatterns(timeSeries, activeQuery)),
153 | );
154 |
155 | // Seasonal patterns
156 | patterns.push(
157 | ...(await this.detectSeasonalPatterns(timeSeries, activeQuery)),
158 | );
159 |
160 | // Burst patterns
161 | patterns.push(
162 | ...(await this.detectBurstPatterns(timeSeries, activeQuery)),
163 | );
164 |
165 | // Decay patterns
166 | patterns.push(
167 | ...(await this.detectDecayPatterns(timeSeries, activeQuery)),
168 | );
169 |
170 | // Sort by confidence
171 | patterns.sort((a, b) => b.confidence - a.confidence);
172 |
173 | // Cache results
174 | this.patternCache.set(cacheKey, patterns);
175 |
176 | this.emit("patterns_analyzed", {
177 | query: activeQuery,
178 | patterns: patterns.length,
179 | highConfidence: patterns.filter((p) => p.confidence > 0.7).length,
180 | });
181 |
182 | return patterns;
183 | } catch (error) {
184 | this.emit("analysis_error", {
185 | error: error instanceof Error ? error.message : String(error),
186 | });
187 | throw error;
188 | }
189 | }
190 |
191 | /**
192 | * Get temporal metrics for a time range
193 | */
194 | async getTemporalMetrics(query?: TemporalQuery): Promise<TemporalMetrics> {
195 | const defaultQuery: TemporalQuery = {
196 | granularity: "day",
197 | aggregation: "count",
198 | timeRange: this.getDefaultTimeRange(),
199 | };
200 |
201 | const activeQuery = { ...defaultQuery, ...query };
202 | const cacheKey = this.generateCacheKey("metrics", activeQuery);
203 |
204 | if (this.metricsCache.has(cacheKey)) {
205 | return this.metricsCache.get(cacheKey)!;
206 | }
207 |
208 | const timeSeries = await this.buildTimeSeries(activeQuery);
209 |
210 | // Calculate activity level
211 | const totalActivity = timeSeries.reduce(
212 | (sum, point) => sum + point.value,
213 | 0,
214 | );
215 | const maxPossibleActivity =
216 | timeSeries.length * Math.max(...timeSeries.map((p) => p.value));
217 | const activityLevel =
218 | maxPossibleActivity > 0 ? totalActivity / maxPossibleActivity : 0;
219 |
220 | // Calculate growth rate
221 | const firstHalf = timeSeries.slice(0, Math.floor(timeSeries.length / 2));
222 | const secondHalf = timeSeries.slice(Math.floor(timeSeries.length / 2));
223 | const firstHalfAvg =
224 | firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;
225 | const secondHalfAvg =
226 | secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;
227 | const growthRate =
228 | firstHalfAvg > 0
229 | ? ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100
230 | : 0;
231 |
232 | // Find peak activity
233 | const peakPoint = timeSeries.reduce((max, point) =>
234 | point.value > max.value ? point : max,
235 | );
236 | const peakActivity = {
237 | timestamp: peakPoint.timestamp,
238 | count: peakPoint.value,
239 | };
240 |
241 | // Calculate average interval
242 | const intervals = [];
243 | for (let i = 1; i < timeSeries.length; i++) {
244 | intervals.push(
245 | timeSeries[i].timestamp.getTime() -
246 | timeSeries[i - 1].timestamp.getTime(),
247 | );
248 | }
249 | const averageInterval =
250 | intervals.length > 0
251 | ? intervals.reduce((sum, interval) => sum + interval, 0) /
252 | intervals.length
253 | : 0;
254 |
255 | // Calculate consistency (inverse of coefficient of variation)
256 | const values = timeSeries.map((p) => p.value);
257 | const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
258 | const variance =
259 | values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
260 | values.length;
261 | const stdDev = Math.sqrt(variance);
262 | const consistency = mean > 0 ? Math.max(0, 1 - stdDev / mean) : 0;
263 |
264 | // Calculate cyclical strength using autocorrelation
265 | const cyclicalStrength = this.calculateCyclicalStrength(values);
266 |
267 | const metrics: TemporalMetrics = {
268 | activityLevel,
269 | growthRate,
270 | peakActivity,
271 | averageInterval,
272 | consistency,
273 | cyclicalStrength,
274 | };
275 |
276 | this.metricsCache.set(cacheKey, metrics);
277 | return metrics;
278 | }
279 |
280 | /**
281 | * Make predictions based on temporal patterns
282 | */
283 | async predictFutureActivity(
284 | query?: TemporalQuery,
285 | ): Promise<PredictionResult> {
286 | const defaultQuery: TemporalQuery = {
287 | granularity: "day",
288 | aggregation: "count",
289 | timeRange: this.getDefaultTimeRange(),
290 | };
291 |
292 | const activeQuery = { ...defaultQuery, ...query };
293 | const cacheKey = this.generateCacheKey("predictions", activeQuery);
294 |
295 | if (this.predictionCache.has(cacheKey)) {
296 | return this.predictionCache.get(cacheKey)!;
297 | }
298 |
299 | // Get historical patterns and metrics
300 | const patterns = await this.analyzeTemporalPatterns(activeQuery);
301 | const metrics = await this.getTemporalMetrics(activeQuery);
302 | const timeSeries = await this.buildTimeSeries(activeQuery);
303 |
304 | // Predict next activity window
305 | const nextActivity = await this.predictNextActivity(
306 | timeSeries,
307 | patterns,
308 | metrics,
309 | );
310 |
311 | // Categorize trends
312 | const shortTermPatterns = patterns.filter((p) =>
313 | this.isShortTerm(p, activeQuery),
314 | );
315 | const longTermPatterns = patterns.filter((p) =>
316 | this.isLongTerm(p, activeQuery),
317 | );
318 |
319 | // Detect anomalies
320 | const anomalies = await this.detectAnomalies(timeSeries, patterns);
321 |
322 | // Generate recommendations
323 | const recommendations = this.generateRecommendations(
324 | patterns,
325 | metrics,
326 | anomalies,
327 | );
328 |
329 | const result: PredictionResult = {
330 | nextActivity,
331 | trends: {
332 | shortTerm: shortTermPatterns,
333 | longTerm: longTermPatterns,
334 | },
335 | anomalies,
336 | recommendations,
337 | };
338 |
339 | this.predictionCache.set(cacheKey, result);
340 | return result;
341 | }
342 |
343 | /**
344 | * Get temporal insights and actionable recommendations
345 | */
346 | async getTemporalInsights(query?: TemporalQuery): Promise<TemporalInsight[]> {
347 | const patterns = await this.analyzeTemporalPatterns(query);
348 | const metrics = await this.getTemporalMetrics(query);
349 | const predictions = await this.predictFutureActivity(query);
350 |
351 | const insights: TemporalInsight[] = [];
352 |
353 | // Pattern-based insights
354 | for (const pattern of patterns.filter((p) => p.confidence > 0.6)) {
355 | insights.push({
356 | type: "pattern",
357 | title: `${
358 | pattern.type.charAt(0).toUpperCase() + pattern.type.slice(1)
359 | } Pattern Detected`,
360 | description: pattern.description,
361 | confidence: pattern.confidence,
362 | timeframe: this.getPatternTimeframe(pattern),
363 | actionable: this.isActionablePattern(pattern),
364 | recommendations: this.getPatternRecommendations(pattern),
365 | });
366 | }
367 |
368 | // Trend insights
369 | if (metrics.growthRate > 20) {
370 | insights.push({
371 | type: "trend",
372 | title: "Increasing Activity Trend",
373 | description: `Memory activity has increased by ${metrics.growthRate.toFixed(
374 | 1,
375 | )}% over the analysis period`,
376 | confidence: 0.8,
377 | timeframe: query?.timeRange || this.getDefaultTimeRange(),
378 | actionable: true,
379 | recommendations: [
380 | "Consider optimizing memory storage for increased load",
381 | "Monitor system performance as activity grows",
382 | "Evaluate current pruning policies",
383 | ],
384 | });
385 | }
386 |
387 | // Anomaly insights
388 | for (const anomaly of predictions.anomalies.filter(
389 | (a) => a.severity > 0.7,
390 | )) {
391 | insights.push({
392 | type: "anomaly",
393 | title: `${
394 | anomaly.type.charAt(0).toUpperCase() + anomaly.type.slice(1)
395 | } Anomaly`,
396 | description: anomaly.description,
397 | confidence: anomaly.severity,
398 | timeframe: {
399 | start: anomaly.timestamp,
400 | end: anomaly.timestamp,
401 | duration: 0,
402 | label: "Point Anomaly",
403 | },
404 | actionable: true,
405 | recommendations: this.getAnomalyRecommendations(anomaly),
406 | });
407 | }
408 |
409 | // Prediction insights
410 | if (predictions.nextActivity.probability > 0.7) {
411 | insights.push({
412 | type: "prediction",
413 | title: "High Probability Activity Window",
414 | description: `${(predictions.nextActivity.probability * 100).toFixed(
415 | 1,
416 | )}% chance of ${predictions.nextActivity.expectedCount} activities`,
417 | confidence: predictions.nextActivity.confidence,
418 | timeframe: predictions.nextActivity.timeRange,
419 | actionable: true,
420 | recommendations: [
421 | "Prepare system for predicted activity surge",
422 | "Consider pre-emptive optimization",
423 | "Monitor resource utilization during predicted window",
424 | ],
425 | });
426 | }
427 |
428 | // Sort by confidence and actionability
429 | insights.sort((a, b) => {
430 | if (a.actionable !== b.actionable) {
431 | return a.actionable ? -1 : 1;
432 | }
433 | return b.confidence - a.confidence;
434 | });
435 |
436 | return insights;
437 | }
438 |
439 | /**
440 | * Build time series data from memory entries
441 | */
442 | private async buildTimeSeries(
443 | query: TemporalQuery,
444 | ): Promise<Array<{ timestamp: Date; value: number; metadata?: any }>> {
445 | const entries = await this.getFilteredEntries(query);
446 | const timeRange = query.timeRange || this.getDefaultTimeRange();
447 |
448 | // Create time buckets based on granularity
449 | const buckets = this.createTimeBuckets(timeRange, query.granularity);
450 | const timeSeries: Array<{
451 | timestamp: Date;
452 | value: number;
453 | metadata?: any;
454 | }> = [];
455 |
456 | for (const bucket of buckets) {
457 | const bucketEntries = entries.filter((entry) => {
458 | const entryTime = new Date(entry.timestamp);
459 | return entryTime >= bucket.start && entryTime < bucket.end;
460 | });
461 |
462 | let value = 0;
463 | const metadata: any = {};
464 |
465 | switch (query.aggregation) {
466 | case "count":
467 | value = bucketEntries.length;
468 | break;
469 | case "success_rate": {
470 | const successful = bucketEntries.filter(
471 | (e) => e.data.outcome === "success" || e.data.success === true,
472 | ).length;
473 | value =
474 | bucketEntries.length > 0 ? successful / bucketEntries.length : 0;
475 | break;
476 | }
477 | case "activity_level":
478 | // Custom metric based on entry types and interactions
479 | value = this.calculateActivityLevel(bucketEntries);
480 | break;
481 | case "diversity": {
482 | const uniqueTypes = new Set(bucketEntries.map((e) => e.type));
483 | value = uniqueTypes.size;
484 | break;
485 | }
486 | }
487 |
488 | // Add metadata
489 | metadata.entryCount = bucketEntries.length;
490 | metadata.types = [...new Set(bucketEntries.map((e) => e.type))];
491 |
492 | timeSeries.push({
493 | timestamp: bucket.start,
494 | value,
495 | metadata,
496 | });
497 | }
498 |
499 | // Apply smoothing if requested
500 | if (query.smoothing?.enabled) {
501 | return this.applySmoothingToTimeSeries(timeSeries, query.smoothing);
502 | }
503 |
504 | return timeSeries;
505 | }
506 |
507 | /**
508 | * Get filtered entries based on query
509 | */
510 | private async getFilteredEntries(
511 | query: TemporalQuery,
512 | ): Promise<MemoryEntry[]> {
513 | let entries = await this.storage.getAll();
514 |
515 | // Apply time range filter
516 | if (query.timeRange) {
517 | entries = entries.filter((entry) => {
518 | const entryTime = new Date(entry.timestamp);
519 | return (
520 | entryTime >= query.timeRange!.start &&
521 | entryTime <= query.timeRange!.end
522 | );
523 | });
524 | }
525 |
526 | // Apply filters
527 | if (query.filters) {
528 | if (query.filters.types) {
529 | entries = entries.filter((entry) =>
530 | query.filters!.types!.includes(entry.type),
531 | );
532 | }
533 |
534 | if (query.filters.projects) {
535 | entries = entries.filter((entry) =>
536 | query.filters!.projects!.some(
537 | (project) =>
538 | entry.data.projectPath?.includes(project) ||
539 | entry.data.projectId === project,
540 | ),
541 | );
542 | }
543 |
544 | if (query.filters.outcomes) {
545 | entries = entries.filter(
546 | (entry) =>
547 | query.filters!.outcomes!.includes(entry.data.outcome) ||
548 | (entry.data.success === true &&
549 | query.filters!.outcomes!.includes("success")) ||
550 | (entry.data.success === false &&
551 | query.filters!.outcomes!.includes("failure")),
552 | );
553 | }
554 |
555 | if (query.filters.tags) {
556 | entries = entries.filter(
557 | (entry) =>
558 | entry.tags?.some((tag) => query.filters!.tags!.includes(tag)),
559 | );
560 | }
561 | }
562 |
563 | return entries;
564 | }
565 |
566 | /**
567 | * Create time buckets for analysis
568 | */
569 | private createTimeBuckets(
570 | timeRange: TimeWindow,
571 | granularity: string,
572 | ): TimeWindow[] {
573 | const buckets: TimeWindow[] = [];
574 | let current = new Date(timeRange.start);
575 | const end = new Date(timeRange.end);
576 |
577 | while (current < end) {
578 | const bucketStart = new Date(current);
579 | let bucketEnd: Date;
580 |
581 | switch (granularity) {
582 | case "hour":
583 | bucketEnd = new Date(current.getTime() + 60 * 60 * 1000);
584 | break;
585 | case "day":
586 | bucketEnd = new Date(current.getTime() + 24 * 60 * 60 * 1000);
587 | break;
588 | case "week":
589 | bucketEnd = new Date(current.getTime() + 7 * 24 * 60 * 60 * 1000);
590 | break;
591 | case "month":
592 | bucketEnd = new Date(
593 | current.getFullYear(),
594 | current.getMonth() + 1,
595 | 1,
596 | );
597 | break;
598 | case "year":
599 | bucketEnd = new Date(current.getFullYear() + 1, 0, 1);
600 | break;
601 | default:
602 | bucketEnd = new Date(current.getTime() + 24 * 60 * 60 * 1000);
603 | }
604 |
605 | if (bucketEnd > end) {
606 | bucketEnd = new Date(end);
607 | }
608 |
609 | buckets.push({
610 | start: bucketStart,
611 | end: bucketEnd,
612 | duration: bucketEnd.getTime() - bucketStart.getTime(),
613 | label: this.formatTimeLabel(bucketStart, granularity),
614 | });
615 |
616 | current = bucketEnd;
617 | }
618 |
619 | return buckets;
620 | }
621 |
622 | /**
623 | * Detect periodic patterns in time series
624 | */
625 | private async detectPeriodicPatterns(
626 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
627 | query: TemporalQuery,
628 | ): Promise<TemporalPattern[]> {
629 | const patterns: TemporalPattern[] = [];
630 | const values = timeSeries.map((p) => p.value);
631 |
632 | // Check for different periods (daily, weekly, monthly cycles)
633 | const periods = [1, 7, 30, 365]; // days
634 |
635 | for (const period of periods) {
636 | const adjustedPeriod = this.adjustPeriodForGranularity(
637 | period,
638 | query.granularity,
639 | );
640 | if (adjustedPeriod >= values.length / 3) continue; // Need at least 3 cycles
641 |
642 | const correlation = this.calculateAutocorrelation(values, adjustedPeriod);
643 |
644 | if (correlation > 0.6) {
645 | patterns.push({
646 | type: "periodic",
647 | confidence: correlation,
648 | period: period * 24 * 60 * 60 * 1000, // Convert to milliseconds
649 | description: `${period}-${query.granularity} cycle detected with ${(
650 | correlation * 100
651 | ).toFixed(1)}% correlation`,
652 | dataPoints: timeSeries,
653 | });
654 | }
655 | }
656 |
657 | return patterns;
658 | }
659 |
660 | /**
661 | * Detect trend patterns
662 | */
663 | private async detectTrendPatterns(
664 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
665 | _query: TemporalQuery,
666 | ): Promise<TemporalPattern[]> {
667 | const patterns: TemporalPattern[] = [];
668 | const values = timeSeries.map((p) => p.value);
669 |
670 | if (values.length < 5) return patterns;
671 |
672 | // Calculate linear regression
673 | const { slope, rSquared } = this.calculateLinearRegression(values);
674 |
675 | if (rSquared > 0.5) {
676 | // Good fit
677 | const trend =
678 | slope > 0.01 ? "increasing" : slope < -0.01 ? "decreasing" : "stable";
679 |
680 | if (trend !== "stable") {
681 | patterns.push({
682 | type: "trending",
683 | confidence: rSquared,
684 | trend,
685 | description: `${trend} trend detected with R² = ${rSquared.toFixed(
686 | 3,
687 | )}`,
688 | dataPoints: timeSeries,
689 | });
690 | }
691 | }
692 |
693 | return patterns;
694 | }
695 |
696 | /**
697 | * Detect seasonal patterns
698 | */
699 | private async detectSeasonalPatterns(
700 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
701 | query: TemporalQuery,
702 | ): Promise<TemporalPattern[]> {
703 | const patterns: TemporalPattern[] = [];
704 |
705 | // Check for daily patterns (hour of day)
706 | if (query.granularity === "hour") {
707 | const hourlyPattern = this.analyzeHourlyPattern(timeSeries);
708 | if (hourlyPattern.confidence > 0.6) {
709 | patterns.push({
710 | type: "seasonal",
711 | confidence: hourlyPattern.confidence,
712 | seasonality: "daily",
713 | description: `Daily pattern: peak activity at ${hourlyPattern.peakHour}:00`,
714 | dataPoints: timeSeries,
715 | });
716 | }
717 | }
718 |
719 | // Check for weekly patterns (day of week)
720 | if (["hour", "day"].includes(query.granularity)) {
721 | const weeklyPattern = this.analyzeWeeklyPattern(timeSeries);
722 | if (weeklyPattern.confidence > 0.6) {
723 | patterns.push({
724 | type: "seasonal",
725 | confidence: weeklyPattern.confidence,
726 | seasonality: "weekly",
727 | description: `Weekly pattern: peak activity on ${weeklyPattern.peakDay}`,
728 | dataPoints: timeSeries,
729 | });
730 | }
731 | }
732 |
733 | return patterns;
734 | }
735 |
736 | /**
737 | * Detect burst patterns (sudden spikes)
738 | */
739 | private async detectBurstPatterns(
740 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
741 | _query: TemporalQuery,
742 | ): Promise<TemporalPattern[]> {
743 | const patterns: TemporalPattern[] = [];
744 | const values = timeSeries.map((p) => p.value);
745 |
746 | const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
747 | const stdDev = Math.sqrt(
748 | values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
749 | values.length,
750 | );
751 |
752 | const threshold = mean + 2 * stdDev; // 2 standard deviations above mean
753 |
754 | const bursts = [];
755 | for (let i = 0; i < values.length; i++) {
756 | if (values[i] > threshold) {
757 | bursts.push(i);
758 | }
759 | }
760 |
761 | if (bursts.length > 0 && bursts.length < values.length * 0.1) {
762 | // Bursts are rare
763 | const confidence = Math.min(0.9, bursts.length / (values.length * 0.05));
764 |
765 | patterns.push({
766 | type: "burst",
767 | confidence,
768 | description: `${bursts.length} burst events detected (${(
769 | threshold / mean
770 | ).toFixed(1)}x normal activity)`,
771 | dataPoints: bursts.map((i) => timeSeries[i]),
772 | });
773 | }
774 |
775 | return patterns;
776 | }
777 |
778 | /**
779 | * Detect decay patterns (gradual decline)
780 | */
781 | private async detectDecayPatterns(
782 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
783 | _query: TemporalQuery,
784 | ): Promise<TemporalPattern[]> {
785 | const patterns: TemporalPattern[] = [];
786 | const values = timeSeries.map((p) => p.value);
787 |
788 | if (values.length < 10) return patterns;
789 |
790 | // Look for exponential decay pattern
791 | const logValues = values.map((v) => Math.log(Math.max(v, 0.1))); // Avoid log(0)
792 | const { slope, rSquared } = this.calculateLinearRegression(logValues);
793 |
794 | if (slope < -0.05 && rSquared > 0.7) {
795 | // Significant decay with good fit
796 | patterns.push({
797 | type: "decay",
798 | confidence: rSquared,
799 | description: `Exponential decay detected (half-life ≈ ${(
800 | -0.693 / slope
801 | ).toFixed(1)} periods)`,
802 | dataPoints: timeSeries,
803 | });
804 | }
805 |
806 | return patterns;
807 | }
808 |
809 | /**
810 | * Calculate activity level for a set of entries
811 | */
812 | private calculateActivityLevel(entries: MemoryEntry[]): number {
813 | if (entries.length === 0) return 0;
814 |
815 | let score = 0;
816 |
817 | // Base score from count
818 | score += Math.min(1, entries.length / 10); // Cap at 10 entries = 1.0
819 |
820 | // Bonus for diversity
821 | const uniqueTypes = new Set(entries.map((e) => e.type));
822 | score += uniqueTypes.size * 0.1;
823 |
824 | // Bonus for successful outcomes
825 | const successful = entries.filter(
826 | (e) => e.data.outcome === "success" || e.data.success === true,
827 | ).length;
828 | score += (successful / entries.length) * 0.3;
829 |
830 | return Math.min(1, score);
831 | }
832 |
833 | /**
834 | * Apply smoothing to time series data
835 | */
836 | private applySmoothingToTimeSeries(
837 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
838 | smoothing: { method: string; window: number },
839 | ): Array<{ timestamp: Date; value: number; metadata?: any }> {
840 | const values = timeSeries.map((p) => p.value);
841 | let smoothedValues: number[];
842 |
843 | switch (smoothing.method) {
844 | case "moving_average":
845 | smoothedValues = this.applyMovingAverage(values, smoothing.window);
846 | break;
847 | case "exponential":
848 | smoothedValues = this.applyExponentialSmoothing(values, 0.3);
849 | break;
850 | default:
851 | smoothedValues = values;
852 | }
853 |
854 | return timeSeries.map((point, i) => ({
855 | ...point,
856 | value: smoothedValues[i],
857 | }));
858 | }
859 |
860 | /**
861 | * Apply moving average smoothing
862 | */
863 | private applyMovingAverage(values: number[], window: number): number[] {
864 | const smoothed: number[] = [];
865 |
866 | for (let i = 0; i < values.length; i++) {
867 | const start = Math.max(0, i - Math.floor(window / 2));
868 | const end = Math.min(values.length, i + Math.ceil(window / 2));
869 | const windowValues = values.slice(start, end);
870 | const average =
871 | windowValues.reduce((sum, val) => sum + val, 0) / windowValues.length;
872 | smoothed.push(average);
873 | }
874 |
875 | return smoothed;
876 | }
877 |
878 | /**
879 | * Apply exponential smoothing
880 | */
881 | private applyExponentialSmoothing(values: number[], alpha: number): number[] {
882 | const smoothed: number[] = [values[0]];
883 |
884 | for (let i = 1; i < values.length; i++) {
885 | smoothed.push(alpha * values[i] + (1 - alpha) * smoothed[i - 1]);
886 | }
887 |
888 | return smoothed;
889 | }
890 |
891 | /**
892 | * Calculate autocorrelation for periodic pattern detection
893 | */
894 | private calculateAutocorrelation(values: number[], lag: number): number {
895 | if (lag >= values.length) return 0;
896 |
897 | const n = values.length - lag;
898 | const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
899 |
900 | let numerator = 0;
901 | let denominator = 0;
902 |
903 | for (let i = 0; i < n; i++) {
904 | numerator += (values[i] - mean) * (values[i + lag] - mean);
905 | }
906 |
907 | for (let i = 0; i < values.length; i++) {
908 | denominator += Math.pow(values[i] - mean, 2);
909 | }
910 |
911 | return denominator > 0 ? numerator / denominator : 0;
912 | }
913 |
914 | /**
915 | * Calculate cyclical strength using autocorrelation
916 | */
917 | private calculateCyclicalStrength(values: number[]): number {
918 | const maxLag = Math.min(values.length / 3, 30);
919 | let maxCorrelation = 0;
920 |
921 | for (let lag = 1; lag < maxLag; lag++) {
922 | const correlation = Math.abs(this.calculateAutocorrelation(values, lag));
923 | maxCorrelation = Math.max(maxCorrelation, correlation);
924 | }
925 |
926 | return maxCorrelation;
927 | }
928 |
929 | /**
930 | * Calculate linear regression
931 | */
932 | private calculateLinearRegression(values: number[]): {
933 | slope: number;
934 | intercept: number;
935 | rSquared: number;
936 | } {
937 | const n = values.length;
938 | const x = Array.from({ length: n }, (_, i) => i);
939 |
940 | const sumX = x.reduce((sum, val) => sum + val, 0);
941 | const sumY = values.reduce((sum, val) => sum + val, 0);
942 | const sumXY = x.reduce((sum, val, i) => sum + val * values[i], 0);
943 | const sumXX = x.reduce((sum, val) => sum + val * val, 0);
944 |
945 | const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
946 | const intercept = (sumY - slope * sumX) / n;
947 |
948 | // Calculate R²
949 | const meanY = sumY / n;
950 | const ssRes = values.reduce((sum, val, i) => {
951 | const predicted = slope * i + intercept;
952 | return sum + Math.pow(val - predicted, 2);
953 | }, 0);
954 | const ssTot = values.reduce(
955 | (sum, val) => sum + Math.pow(val - meanY, 2),
956 | 0,
957 | );
958 | const rSquared = ssTot > 0 ? 1 - ssRes / ssTot : 0;
959 |
960 | return { slope, intercept, rSquared };
961 | }
962 |
963 | /**
964 | * Predict next activity window
965 | */
966 | private async predictNextActivity(
967 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
968 | patterns: TemporalPattern[],
969 | _metrics: TemporalMetrics,
970 | ): Promise<PredictionResult["nextActivity"]> {
971 | const lastPoint = timeSeries[timeSeries.length - 1];
972 | const averageValue =
973 | timeSeries.reduce((sum, p) => sum + p.value, 0) / timeSeries.length;
974 |
975 | // Base prediction on recent trend
976 | let expectedCount = averageValue;
977 | let probability = 0.5;
978 |
979 | // Adjust based on trends
980 | const trendPattern = patterns.find((p) => p.type === "trending");
981 | if (trendPattern && trendPattern.trend === "increasing") {
982 | expectedCount *= 1.2;
983 | probability += 0.2;
984 | } else if (trendPattern && trendPattern.trend === "decreasing") {
985 | expectedCount *= 0.8;
986 | probability -= 0.1;
987 | }
988 |
989 | // Adjust based on periodic patterns
990 | const periodicPattern = patterns.find(
991 | (p) => p.type === "periodic" && p.confidence > 0.7,
992 | );
993 | if (periodicPattern) {
994 | probability += 0.3;
995 | }
996 |
997 | // Determine time range for next activity (next period based on granularity)
998 | const nextStart = new Date(
999 | lastPoint.timestamp.getTime() + 24 * 60 * 60 * 1000,
1000 | ); // Next day
1001 | const nextEnd = new Date(nextStart.getTime() + 24 * 60 * 60 * 1000);
1002 |
1003 | return {
1004 | probability: Math.min(0.95, Math.max(0.05, probability)),
1005 | timeRange: {
1006 | start: nextStart,
1007 | end: nextEnd,
1008 | duration: 24 * 60 * 60 * 1000,
1009 | label: "Next 24 hours",
1010 | },
1011 | expectedCount: Math.round(expectedCount),
1012 | confidence: Math.min(
1013 | 0.9,
1014 | patterns.reduce((sum, p) => sum + p.confidence, 0) / patterns.length,
1015 | ),
1016 | };
1017 | }
1018 |
1019 | /**
1020 | * Detect anomalies in time series
1021 | */
1022 | private async detectAnomalies(
1023 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
1024 | _patterns: TemporalPattern[],
1025 | ): Promise<PredictionResult["anomalies"]> {
1026 | const anomalies: PredictionResult["anomalies"] = [];
1027 | const values = timeSeries.map((p) => p.value);
1028 |
1029 | const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
1030 | const stdDev = Math.sqrt(
1031 | values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
1032 | values.length,
1033 | );
1034 |
1035 | for (let i = 0; i < timeSeries.length; i++) {
1036 | const point = timeSeries[i];
1037 | const value = point.value;
1038 |
1039 | // Spike detection
1040 | if (value > mean + 3 * stdDev) {
1041 | anomalies.push({
1042 | timestamp: point.timestamp,
1043 | type: "spike",
1044 | severity: Math.min(1, (value - mean) / (3 * stdDev)),
1045 | description: `Activity spike: ${value} (${(
1046 | (value / mean - 1) *
1047 | 100
1048 | ).toFixed(0)}% above normal)`,
1049 | });
1050 | }
1051 |
1052 | // Drought detection
1053 | if (value < mean - 2 * stdDev && mean > 1) {
1054 | anomalies.push({
1055 | timestamp: point.timestamp,
1056 | type: "drought",
1057 | severity: Math.min(1, (mean - value) / (2 * stdDev)),
1058 | description: `Activity drought: ${value} (${(
1059 | (1 - value / mean) *
1060 | 100
1061 | ).toFixed(0)}% below normal)`,
1062 | });
1063 | }
1064 | }
1065 |
1066 | // Detect regime shifts (significant changes in mean)
1067 | const shifts = this.detectRegimeShifts(timeSeries);
1068 | anomalies.push(...shifts);
1069 |
1070 | return anomalies.sort((a, b) => b.severity - a.severity);
1071 | }
1072 |
1073 | /**
1074 | * Detect regime shifts in time series
1075 | */
1076 | private detectRegimeShifts(
1077 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
1078 | ): Array<{
1079 | timestamp: Date;
1080 | type: "shift";
1081 | severity: number;
1082 | description: string;
1083 | }> {
1084 | const shifts: Array<{
1085 | timestamp: Date;
1086 | type: "shift";
1087 | severity: number;
1088 | description: string;
1089 | }> = [];
1090 | const values = timeSeries.map((p) => p.value);
1091 |
1092 | if (values.length < 20) return shifts; // Need sufficient data
1093 |
1094 | const windowSize = Math.floor(values.length / 4);
1095 |
1096 | for (let i = windowSize; i < values.length - windowSize; i++) {
1097 | const before = values.slice(i - windowSize, i);
1098 | const after = values.slice(i, i + windowSize);
1099 |
1100 | const meanBefore =
1101 | before.reduce((sum, val) => sum + val, 0) / before.length;
1102 | const meanAfter = after.reduce((sum, val) => sum + val, 0) / after.length;
1103 |
1104 | const changeMagnitude = Math.abs(meanAfter - meanBefore);
1105 | const relativeChange = meanBefore > 0 ? changeMagnitude / meanBefore : 0;
1106 |
1107 | if (relativeChange > 0.5) {
1108 | // 50% change
1109 | shifts.push({
1110 | timestamp: timeSeries[i].timestamp,
1111 | type: "shift",
1112 | severity: Math.min(1, relativeChange),
1113 | description: `Regime shift: ${meanBefore.toFixed(
1114 | 1,
1115 | )} → ${meanAfter.toFixed(1)} (${(relativeChange * 100).toFixed(
1116 | 0,
1117 | )}% change)`,
1118 | });
1119 | }
1120 | }
1121 |
1122 | return shifts;
1123 | }
1124 |
1125 | /**
1126 | * Generate recommendations based on analysis
1127 | */
1128 | private generateRecommendations(
1129 | patterns: TemporalPattern[],
1130 | metrics: TemporalMetrics,
1131 | anomalies: PredictionResult["anomalies"],
1132 | ): string[] {
1133 | const recommendations: string[] = [];
1134 |
1135 | // Pattern-based recommendations
1136 | const periodicPattern = patterns.find(
1137 | (p) => p.type === "periodic" && p.confidence > 0.7,
1138 | );
1139 | if (periodicPattern) {
1140 | recommendations.push(
1141 | "Schedule maintenance and optimizations during low-activity periods based on detected cycles",
1142 | );
1143 | }
1144 |
1145 | const trendPattern = patterns.find((p) => p.type === "trending");
1146 | if (trendPattern?.trend === "increasing") {
1147 | recommendations.push(
1148 | "Plan for increased storage and processing capacity based on growing activity trend",
1149 | );
1150 | } else if (trendPattern?.trend === "decreasing") {
1151 | recommendations.push(
1152 | "Investigate causes of declining activity and consider engagement strategies",
1153 | );
1154 | }
1155 |
1156 | // Metrics-based recommendations
1157 | if (metrics.consistency < 0.5) {
1158 | recommendations.push(
1159 | "High variability detected - consider implementing activity smoothing mechanisms",
1160 | );
1161 | }
1162 |
1163 | if (metrics.growthRate > 50) {
1164 | recommendations.push(
1165 | "Rapid growth detected - implement proactive scaling measures",
1166 | );
1167 | }
1168 |
1169 | // Anomaly-based recommendations
1170 | const spikes = anomalies.filter(
1171 | (a) => a.type === "spike" && a.severity > 0.7,
1172 | );
1173 | if (spikes.length > 0) {
1174 | recommendations.push(
1175 | "Implement burst handling to manage activity spikes effectively",
1176 | );
1177 | }
1178 |
1179 | const droughts = anomalies.filter(
1180 | (a) => a.type === "drought" && a.severity > 0.7,
1181 | );
1182 | if (droughts.length > 0) {
1183 | recommendations.push(
1184 | "Investigate causes of activity droughts and implement retention strategies",
1185 | );
1186 | }
1187 |
1188 | return recommendations;
1189 | }
1190 |
1191 | /**
1192 | * Utility methods
1193 | */
1194 | private getDefaultTimeRange(): TimeWindow {
1195 | const end = new Date();
1196 | const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
1197 |
1198 | return {
1199 | start,
1200 | end,
1201 | duration: end.getTime() - start.getTime(),
1202 | label: "Last 30 days",
1203 | };
1204 | }
1205 |
1206 | private generateCacheKey(type: string, query: TemporalQuery): string {
1207 | return `${type}_${JSON.stringify(query)}`;
1208 | }
1209 |
1210 | private adjustPeriodForGranularity(
1211 | period: number,
1212 | granularity: string,
1213 | ): number {
1214 | switch (granularity) {
1215 | case "hour":
1216 | return period * 24;
1217 | case "day":
1218 | return period;
1219 | case "week":
1220 | return Math.ceil(period / 7);
1221 | case "month":
1222 | return Math.ceil(period / 30);
1223 | case "year":
1224 | return Math.ceil(period / 365);
1225 | default:
1226 | return period;
1227 | }
1228 | }
1229 |
1230 | private formatTimeLabel(date: Date, granularity: string): string {
1231 | switch (granularity) {
1232 | case "hour":
1233 | return date.toISOString().slice(0, 13) + ":00";
1234 | case "day":
1235 | return date.toISOString().slice(0, 10);
1236 | case "week":
1237 | return `Week of ${date.toISOString().slice(0, 10)}`;
1238 | case "month":
1239 | return `${date.getFullYear()}-${(date.getMonth() + 1)
1240 | .toString()
1241 | .padStart(2, "0")}`;
1242 | case "year":
1243 | return date.getFullYear().toString();
1244 | default:
1245 | return date.toISOString().slice(0, 10);
1246 | }
1247 | }
1248 |
1249 | private isShortTerm(
1250 | pattern: TemporalPattern,
1251 | _query: TemporalQuery,
1252 | ): boolean {
1253 | if (pattern.period) {
1254 | const days = pattern.period / (24 * 60 * 60 * 1000);
1255 | return days <= 7;
1256 | }
1257 | return true;
1258 | }
1259 |
1260 | private isLongTerm(pattern: TemporalPattern, _query: TemporalQuery): boolean {
1261 | if (pattern.period) {
1262 | const days = pattern.period / (24 * 60 * 60 * 1000);
1263 | return days > 30;
1264 | }
1265 | return false;
1266 | }
1267 |
1268 | private getPatternTimeframe(pattern: TemporalPattern): TimeWindow {
1269 | if (pattern.dataPoints.length > 0) {
1270 | const start = pattern.dataPoints[0].timestamp;
1271 | const end = pattern.dataPoints[pattern.dataPoints.length - 1].timestamp;
1272 | return {
1273 | start,
1274 | end,
1275 | duration: end.getTime() - start.getTime(),
1276 | label: `${start.toISOString().slice(0, 10)} to ${end
1277 | .toISOString()
1278 | .slice(0, 10)}`,
1279 | };
1280 | }
1281 | return this.getDefaultTimeRange();
1282 | }
1283 |
1284 | private isActionablePattern(pattern: TemporalPattern): boolean {
1285 | return (
1286 | pattern.confidence > 0.7 &&
1287 | ["periodic", "trending", "seasonal"].includes(pattern.type)
1288 | );
1289 | }
1290 |
1291 | private getPatternRecommendations(pattern: TemporalPattern): string[] {
1292 | const recommendations: string[] = [];
1293 |
1294 | switch (pattern.type) {
1295 | case "periodic":
1296 | recommendations.push(
1297 | "Schedule regular maintenance during low-activity periods",
1298 | );
1299 | recommendations.push(
1300 | "Optimize resource allocation based on predictable cycles",
1301 | );
1302 | break;
1303 | case "trending":
1304 | if (pattern.trend === "increasing") {
1305 | recommendations.push("Plan for capacity expansion");
1306 | recommendations.push("Implement proactive monitoring");
1307 | } else if (pattern.trend === "decreasing") {
1308 | recommendations.push("Investigate root causes of decline");
1309 | recommendations.push("Consider engagement interventions");
1310 | }
1311 | break;
1312 | case "seasonal":
1313 | recommendations.push(
1314 | "Adjust system configuration for seasonal patterns",
1315 | );
1316 | recommendations.push(
1317 | "Plan marketing and engagement around peak periods",
1318 | );
1319 | break;
1320 | }
1321 |
1322 | return recommendations;
1323 | }
1324 |
1325 | private getAnomalyRecommendations(anomaly: {
1326 | type: string;
1327 | severity: number;
1328 | }): string[] {
1329 | const recommendations: string[] = [];
1330 |
1331 | switch (anomaly.type) {
1332 | case "spike":
1333 | recommendations.push("Implement burst protection mechanisms");
1334 | recommendations.push("Investigate spike triggers for prevention");
1335 | recommendations.push("Consider auto-scaling capabilities");
1336 | break;
1337 | case "drought":
1338 | recommendations.push("Implement activity monitoring alerts");
1339 | recommendations.push("Investigate user engagement issues");
1340 | recommendations.push("Consider proactive outreach strategies");
1341 | break;
1342 | case "shift":
1343 | recommendations.push("Investigate underlying system changes");
1344 | recommendations.push("Update baseline metrics and thresholds");
1345 | recommendations.push("Review configuration changes during this period");
1346 | break;
1347 | }
1348 |
1349 | return recommendations;
1350 | }
1351 |
1352 | private analyzeHourlyPattern(
1353 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
1354 | ): { confidence: number; peakHour: number } {
1355 | const hourlyActivity = new Array(24).fill(0);
1356 | const hourlyCounts = new Array(24).fill(0);
1357 |
1358 | for (const point of timeSeries) {
1359 | const hour = point.timestamp.getHours();
1360 | hourlyActivity[hour] += point.value;
1361 | hourlyCounts[hour]++;
1362 | }
1363 |
1364 | // Calculate average activity per hour
1365 | const hourlyAverages = hourlyActivity.map((total, i) =>
1366 | hourlyCounts[i] > 0 ? total / hourlyCounts[i] : 0,
1367 | );
1368 |
1369 | // Find peak hour
1370 | const peakHour = hourlyAverages.indexOf(Math.max(...hourlyAverages));
1371 |
1372 | // Calculate confidence based on variance
1373 | const mean = hourlyAverages.reduce((sum, val) => sum + val, 0) / 24;
1374 | const variance =
1375 | hourlyAverages.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
1376 | 24;
1377 | const stdDev = Math.sqrt(variance);
1378 | const confidence = mean > 0 ? Math.min(0.9, stdDev / mean) : 0;
1379 |
1380 | return { confidence, peakHour };
1381 | }
1382 |
1383 | private analyzeWeeklyPattern(
1384 | timeSeries: Array<{ timestamp: Date; value: number; metadata?: any }>,
1385 | ): { confidence: number; peakDay: string } {
1386 | const weeklyActivity = new Array(7).fill(0);
1387 | const weeklyCounts = new Array(7).fill(0);
1388 | const dayNames = [
1389 | "Sunday",
1390 | "Monday",
1391 | "Tuesday",
1392 | "Wednesday",
1393 | "Thursday",
1394 | "Friday",
1395 | "Saturday",
1396 | ];
1397 |
1398 | for (const point of timeSeries) {
1399 | const day = point.timestamp.getDay();
1400 | weeklyActivity[day] += point.value;
1401 | weeklyCounts[day]++;
1402 | }
1403 |
1404 | // Calculate average activity per day
1405 | const weeklyAverages = weeklyActivity.map((total, i) =>
1406 | weeklyCounts[i] > 0 ? total / weeklyCounts[i] : 0,
1407 | );
1408 |
1409 | // Find peak day
1410 | const peakDayIndex = weeklyAverages.indexOf(Math.max(...weeklyAverages));
1411 | const peakDay = dayNames[peakDayIndex];
1412 |
1413 | // Calculate confidence
1414 | const mean = weeklyAverages.reduce((sum, val) => sum + val, 0) / 7;
1415 | const variance =
1416 | weeklyAverages.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / 7;
1417 | const stdDev = Math.sqrt(variance);
1418 | const confidence = mean > 0 ? Math.min(0.9, stdDev / mean) : 0;
1419 |
1420 | return { confidence, peakDay };
1421 | }
1422 |
1423 | /**
1424 | * Setup periodic analysis
1425 | */
1426 | private setupPeriodicAnalysis(): void {
1427 | // Run analysis every 6 hours
1428 | setInterval(
1429 | async () => {
1430 | try {
1431 | const insights = await this.getTemporalInsights();
1432 | this.emit("periodic_analysis_completed", {
1433 | insights: insights.length,
1434 | });
1435 | } catch (error) {
1436 | this.emit("periodic_analysis_error", {
1437 | error: error instanceof Error ? error.message : String(error),
1438 | });
1439 | }
1440 | },
1441 | 6 * 60 * 60 * 1000,
1442 | );
1443 | }
1444 | }
1445 |
```