Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/memory/kg-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ export async function getMemoryManager(): Promise<MemoryManager> {
return globalMemoryManager!;
}

/**
* Reset the global Knowledge Graph instance
* Useful for testing to ensure clean state between tests
*/
export function resetKnowledgeGraph(): void {
globalKnowledgeGraph = null;
globalKGStorage = null;
globalMemoryManager = null;
currentStorageDir = null;
}

/**
* Convert file extension to language name
*/
Expand Down
8 changes: 7 additions & 1 deletion src/memory/knowledge-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export interface GraphNode {
| "documentation_section"
| "link_validation"
| "sync_event"
| "documentation_freshness_event";
| "documentation_freshness_event"
| "documentation_example"
| "example_validation"
| "call_graph";
label: string;
properties: Record<string, any>;
weight: number;
Expand All @@ -59,6 +62,9 @@ export interface GraphEdge {
| "has_link_validation"
| "requires_fix"
| "project_has_freshness_event"
| "has_example"
| "validates"
| "has_call_graph"
| (string & NonNullable<unknown>); // Allow any string (for timestamped types like "project_deployed_with:2024-...")
weight: number;
properties: Record<string, any>;
Expand Down
167 changes: 164 additions & 3 deletions src/memory/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,82 @@ export type DocumentationFreshnessEventEntity = z.infer<
typeof DocumentationFreshnessEventEntitySchema
>;

/**
* Documentation Example Entity Schema
* Represents a code example in documentation with tracking and validation
*/
export const DocumentationExampleEntitySchema = z.object({
sourceFile: z.string().min(1, "Source file path is required"),
language: z.string().min(1, "Programming language is required"),
code: z.string().min(1, "Code content is required"),
diataxisType: z.enum(["tutorial", "how-to", "reference", "explanation"]),
referencedSymbols: z.array(z.string()).default([]),
lastValidated: z.string().datetime().optional(),
validationStatus: z.enum(["valid", "invalid", "unknown"]).default("unknown"),
exampleId: z.string().min(1, "Example ID is required"),
contentHash: z.string().optional(),
lineStart: z.number().int().min(1).optional(),
lineEnd: z.number().int().min(1).optional(),
});

export type DocumentationExampleEntity = z.infer<
typeof DocumentationExampleEntitySchema
>;

/**
* Example Validation Entity Schema
* Represents validation results for a documentation example
*/
export const ExampleValidationEntitySchema = z.object({
exampleId: z.string().min(1, "Example ID is required"),
validatedAt: z.string().datetime(),
result: z.enum(["pass", "fail", "warning"]),
issues: z.array(z.string()).default([]),
confidenceScore: z.number().min(0).max(1),
validationMethod: z.enum(["ast", "llm", "execution"]),
errorDetails: z.record(z.string(), z.unknown()).optional(),
suggestions: z.array(z.string()).default([]),
});

export type ExampleValidationEntity = z.infer<
typeof ExampleValidationEntitySchema
>;

/**
* Call Graph Entity Schema
* Represents a call graph for code execution analysis
*/
export const CallGraphNodeSchema = z.object({
functionName: z.string().min(1),
filePath: z.string().min(1),
lineNumber: z.number().int().min(1).optional(),
callCount: z.number().int().min(0).default(1),
});

export const CallGraphEdgeSchema = z.object({
from: z.string().min(1),
to: z.string().min(1),
callType: z
.enum(["direct", "indirect", "async", "callback"])
.default("direct"),
weight: z.number().min(0).default(1.0),
});

export const CallGraphEntitySchema = z.object({
rootFunction: z.string().min(1, "Root function is required"),
nodes: z.array(CallGraphNodeSchema).default([]),
edges: z.array(CallGraphEdgeSchema).default([]),
depth: z.number().int().min(0),
generatedAt: z.string().datetime(),
analysisMethod: z.enum(["static", "dynamic", "hybrid"]).default("static"),
totalFunctions: z.number().int().min(0).default(0),
entryPoint: z.string().optional(),
});

export type CallGraphNode = z.infer<typeof CallGraphNodeSchema>;
export type CallGraphEdge = z.infer<typeof CallGraphEdgeSchema>;
export type CallGraphEntity = z.infer<typeof CallGraphEntitySchema>;

// ============================================================================
// Relationship Schemas
// ============================================================================
Expand Down Expand Up @@ -462,6 +538,48 @@ export type ProjectHasFreshnessEventRelationship = z.infer<
typeof ProjectHasFreshnessEventSchema
>;

/**
* Has Example Relationship (Document -> Documentation Example)
* Links a documentation file/section to a code example it contains
*/
export const HasExampleSchema = BaseRelationshipSchema.extend({
type: z.literal("has_example"),
exampleCount: z.number().int().min(0).default(1),
primaryLanguage: z.string().optional(),
exampleType: z
.enum(["inline", "reference", "embedded", "external"])
.default("inline"),
});

export type HasExampleRelationship = z.infer<typeof HasExampleSchema>;

/**
* Validates Relationship (Example Validation -> Documentation Example)
* Links a validation result to the example it validates
*/
export const ValidatesSchema = BaseRelationshipSchema.extend({
type: z.literal("validates"),
validationRun: z.string().datetime(),
previousResult: z.enum(["pass", "fail", "warning", "none"]).optional(),
resultChanged: z.boolean().default(false),
});

export type ValidatesRelationship = z.infer<typeof ValidatesSchema>;

/**
* Has Call Graph Relationship (Documentation Example -> Call Graph)
* Links an example to its execution call graph
*/
export const HasCallGraphSchema = BaseRelationshipSchema.extend({
type: z.literal("has_call_graph"),
graphDepth: z.number().int().min(0),
totalNodes: z.number().int().min(0),
totalEdges: z.number().int().min(0),
complexity: z.enum(["low", "medium", "high"]).optional(),
});

export type HasCallGraphRelationship = z.infer<typeof HasCallGraphSchema>;

// ============================================================================
// Union Types and Type Guards
// ============================================================================
Expand Down Expand Up @@ -499,6 +617,16 @@ const DocumentationFreshnessEventEntityWithType =
DocumentationFreshnessEventEntitySchema.extend({
type: z.literal("documentation_freshness_event"),
});
const DocumentationExampleEntityWithType =
DocumentationExampleEntitySchema.extend({
type: z.literal("documentation_example"),
});
const ExampleValidationEntityWithType = ExampleValidationEntitySchema.extend({
type: z.literal("example_validation"),
});
const CallGraphEntityWithType = CallGraphEntitySchema.extend({
type: z.literal("call_graph"),
});

export const EntitySchema = z.union([
ProjectEntityWithType,
Expand All @@ -511,6 +639,9 @@ export const EntitySchema = z.union([
LinkValidationEntityWithType,
SitemapEntityWithType,
DocumentationFreshnessEventEntityWithType,
DocumentationExampleEntityWithType,
ExampleValidationEntityWithType,
CallGraphEntityWithType,
]);

export type Entity = z.infer<typeof EntitySchema>;
Expand All @@ -532,6 +663,9 @@ export const RelationshipSchema = z.union([
CreatedBySchema,
ProjectHasSitemapSchema,
ProjectHasFreshnessEventSchema,
HasExampleSchema,
ValidatesSchema,
HasCallGraphSchema,
]);

export type Relationship =
Expand All @@ -547,7 +681,10 @@ export type Relationship =
| ResultsInRelationship
| CreatedByRelationship
| ProjectHasSitemapRelationship
| ProjectHasFreshnessEventRelationship;
| ProjectHasFreshnessEventRelationship
| HasExampleRelationship
| ValidatesRelationship
| HasCallGraphRelationship;

// ============================================================================
// Validation Helpers
Expand Down Expand Up @@ -600,14 +737,32 @@ export function isDocumentationSectionEntity(
return entity.type === "documentation_section";
}

export function isDocumentationExampleEntity(
entity: Entity,
): entity is DocumentationExampleEntity & { type: "documentation_example" } {
return entity.type === "documentation_example";
}

export function isExampleValidationEntity(
entity: Entity,
): entity is ExampleValidationEntity & { type: "example_validation" } {
return entity.type === "example_validation";
}

export function isCallGraphEntity(
entity: Entity,
): entity is CallGraphEntity & { type: "call_graph" } {
return entity.type === "call_graph";
}

// ============================================================================
// Schema Metadata
// ============================================================================

/**
* Schema version for migration support
*/
export const SCHEMA_VERSION = "1.0.0";
export const SCHEMA_VERSION = "1.1.0";

/**
* Schema metadata for documentation and validation
Expand All @@ -623,6 +778,9 @@ export const SCHEMA_METADATA = {
"documentation_section",
"technology",
"documentation_freshness_event",
"documentation_example",
"example_validation",
"call_graph",
] as const,
relationshipTypes: [
"project_uses_technology",
Expand All @@ -638,6 +796,9 @@ export const SCHEMA_METADATA = {
"created_by",
"project_has_sitemap",
"project_has_freshness_event",
"has_example",
"validates",
"has_call_graph",
] as const,
lastUpdated: "2025-10-01",
lastUpdated: "2025-12-10",
} as const;
24 changes: 23 additions & 1 deletion tests/integration/memory-mcp-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ import {
getProjectInsights,
getSimilarProjects,
getMemoryStatistics,
resetMemoryManager,
} from "../../src/memory/integration.js";
import { resetKnowledgeGraph } from "../../src/memory/kg-integration.js";
import { analyzeRepository } from "../../src/tools/analyze-repository.js";
import { recommendSSG } from "../../src/tools/recommend-ssg.js";

describe("Memory MCP Tools Integration", () => {
let tempDir: string;
let originalStorageDir: string | undefined;
let testProjectDir: string;

beforeEach(async () => {
Expand All @@ -31,16 +34,35 @@ describe("Memory MCP Tools Integration", () => {
os.tmpdir(),
`memory-mcp-integration-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`,
.substring(2, 11)}`,
);
await fs.mkdir(tempDir, { recursive: true });

// Set storage directory to temp for test isolation
originalStorageDir = process.env.DOCUMCP_STORAGE_DIR;
process.env.DOCUMCP_STORAGE_DIR = path.join(tempDir, "memory");
await fs.mkdir(path.join(tempDir, "memory"), { recursive: true });

// Reset global singletons to use new storage directory
resetKnowledgeGraph();
await resetMemoryManager(path.join(tempDir, "memory"));

// Create a mock project structure for testing
testProjectDir = path.join(tempDir, "test-project");
await createMockProject(testProjectDir);
});

afterEach(async () => {
// Reset global singletons
resetKnowledgeGraph();

// Restore original storage directory
if (originalStorageDir !== undefined) {
process.env.DOCUMCP_STORAGE_DIR = originalStorageDir;
} else {
delete process.env.DOCUMCP_STORAGE_DIR;
}

// Cleanup temp directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
Expand Down
14 changes: 13 additions & 1 deletion tests/memory/kg-storage-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,11 +513,16 @@ describe("KGStorage - Validation and Error Handling", () => {

await storage.saveEntities(entities);

// Verify initial save worked
const initial = await storage.loadEntities();
expect(initial).toHaveLength(1);
expect(initial[0].id).toBe("project:debug");

// Set DEBUG env var
const originalDebug = process.env.DEBUG;
process.env.DEBUG = "true";

// Modify
// Modify - this will create a backup of the original entities
const modifiedEntities: GraphNode[] = [
{
id: "project:modified",
Expand All @@ -530,6 +535,12 @@ describe("KGStorage - Validation and Error Handling", () => {
];
await storage.saveEntities(modifiedEntities);

// Verify backup was created by checking backups directory
const backupDir = join(testDir, "backups");
const backupFiles = await fs.readdir(backupDir);
const entityBackups = backupFiles.filter((f) => f.startsWith("entities"));
expect(entityBackups.length).toBeGreaterThan(0);

// Restore (should log in debug mode)
await storage.restoreFromBackup("entities");

Expand All @@ -542,6 +553,7 @@ describe("KGStorage - Validation and Error Handling", () => {

// Verify restoration worked
const restored = await storage.loadEntities();
expect(restored).toHaveLength(1);
expect(restored[0].id).toBe("project:debug");
});
});
Expand Down
4 changes: 2 additions & 2 deletions tests/memory/kg-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe("KGStorage", () => {

expect(stats.entityCount).toBe(2);
expect(stats.relationshipCount).toBe(1);
expect(stats.schemaVersion).toBe("1.0.0");
expect(stats.schemaVersion).toBe("1.1.0");
expect(stats.fileSize.entities).toBeGreaterThan(0);
});
});
Expand Down Expand Up @@ -420,7 +420,7 @@ describe("KGStorage", () => {
const parsed = JSON.parse(json);

expect(parsed.metadata).toBeDefined();
expect(parsed.metadata.version).toBe("1.0.0");
expect(parsed.metadata.version).toBe("1.1.0");
expect(parsed.entities).toHaveLength(1);
expect(parsed.relationships).toHaveLength(0);
});
Expand Down
Loading
Loading