RelationshipManager.js•12.8 kB
export class RelationshipManager {
constructor(factStore) {
this.factStore = factStore;
this.relationshipTypes = {
prevents: {
name: 'prevents',
description: 'This fact prevents or mitigates the target fact',
inverse: 'prevented_by',
weight: 1.0,
},
enables: {
name: 'enables',
description: 'This fact enables or supports the target fact',
inverse: 'enabled_by',
weight: 0.8,
},
requires: {
name: 'requires',
description: 'This fact requires the target fact as a prerequisite',
inverse: 'required_by',
weight: 0.9,
},
conflicts: {
name: 'conflicts',
description: 'This fact conflicts or contradicts the target fact',
inverse: 'conflicts',
weight: 0.7,
},
extends: {
name: 'extends',
description: 'This fact extends or builds upon the target fact',
inverse: 'extended_by',
weight: 0.6,
},
similar: {
name: 'similar',
description: 'This fact is similar to the target fact',
inverse: 'similar',
weight: 0.5,
},
};
}
async createRelationship(sourceFactId, targetFactId, relationshipType, metadata = {}) {
if (!this.isValidRelationshipType(relationshipType)) {
throw new Error(`Invalid relationship type: ${relationshipType}`);
}
const sourceFact = this.factStore.factsIndex.get(sourceFactId);
const targetFact = this.factStore.factsIndex.get(targetFactId);
if (!sourceFact) {
throw new Error(`Source fact ${sourceFactId} not found`);
}
if (!targetFact) {
throw new Error(`Target fact ${targetFactId} not found`);
}
if (sourceFactId === targetFactId) {
throw new Error('Cannot create relationship to self');
}
const relationship = {
id: `rel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
sourceId: sourceFactId,
targetId: targetFactId,
type: relationshipType,
weight: this.relationshipTypes[relationshipType].weight,
createdAt: new Date().toISOString(),
metadata: {
confidence: metadata.confidence || 0.8,
source: metadata.source || 'manual',
...metadata,
},
};
await this.addRelationshipToFact(sourceFactId, relationship);
const inverse = this.relationshipTypes[relationshipType].inverse;
if (inverse && inverse !== relationshipType) {
const inverseRelationship = {
...relationship,
id: `rel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
sourceId: targetFactId,
targetId: sourceFactId,
type: inverse,
};
await this.addRelationshipToFact(targetFactId, inverseRelationship);
}
return relationship;
}
async addRelationshipToFact(factId, relationship) {
const fact = this.factStore.factsIndex.get(factId);
if (!fact) {
throw new Error(`Fact ${factId} not found`);
}
if (!fact.relationships) {
fact.relationships = [];
}
const existingIndex = fact.relationships.findIndex(
rel => rel.targetId === relationship.targetId && rel.type === relationship.type
);
if (existingIndex >= 0) {
fact.relationships[existingIndex] = relationship;
} else {
fact.relationships.push(relationship);
}
await this.factStore.updateFact(factId, fact);
}
async removeRelationship(sourceFactId, targetFactId, relationshipType) {
const sourceFact = this.factStore.factsIndex.get(sourceFactId);
if (!sourceFact || !sourceFact.relationships) {
return false;
}
const relationshipIndex = sourceFact.relationships.findIndex(
rel => rel.targetId === targetFactId && rel.type === relationshipType
);
if (relationshipIndex === -1) {
return false;
}
sourceFact.relationships.splice(relationshipIndex, 1);
await this.factStore.updateFact(sourceFactId, sourceFact);
const inverse = this.relationshipTypes[relationshipType]?.inverse;
if (inverse && inverse !== relationshipType) {
const targetFact = this.factStore.factsIndex.get(targetFactId);
if (targetFact && targetFact.relationships) {
const inverseIndex = targetFact.relationships.findIndex(
rel => rel.targetId === sourceFactId && rel.type === inverse
);
if (inverseIndex >= 0) {
targetFact.relationships.splice(inverseIndex, 1);
await this.factStore.updateFact(targetFactId, targetFact);
}
}
}
return true;
}
async autoDiscoverRelationships(factId, maxSuggestions = 5) {
const fact = this.factStore.factsIndex.get(factId);
if (!fact) {
throw new Error(`Fact ${factId} not found`);
}
const suggestions = [];
const similarFacts = await this.findSimilarFacts(fact);
const conflictingFacts = await this.findConflictingFacts(fact);
const dependencyFacts = await this.findDependencyFacts(fact);
for (const similarFact of similarFacts.slice(0, 2)) {
suggestions.push({
type: 'similar',
targetId: similarFact.id,
confidence: similarFact.similarity,
reason: 'Content similarity detected',
});
}
for (const conflictingFact of conflictingFacts.slice(0, 2)) {
suggestions.push({
type: 'conflicts',
targetId: conflictingFact.id,
confidence: conflictingFact.conflictScore,
reason: 'Potential conflict detected',
});
}
for (const dependencyFact of dependencyFacts.slice(0, 2)) {
suggestions.push({
type: dependencyFact.relationType,
targetId: dependencyFact.id,
confidence: dependencyFact.dependencyScore,
reason: dependencyFact.reason,
});
}
return suggestions
.sort((a, b) => b.confidence - a.confidence)
.slice(0, maxSuggestions);
}
async findSimilarFacts(fact) {
const allFacts = Array.from(this.factStore.factsIndex.values())
.filter(f => f.id !== fact.id);
const similarities = [];
for (const otherFact of allFacts) {
const similarity = this.calculateContentSimilarity(fact, otherFact);
if (similarity > 0.6) {
similarities.push({
...otherFact,
similarity,
});
}
}
return similarities.sort((a, b) => b.similarity - a.similarity);
}
async findConflictingFacts(fact) {
const allFacts = Array.from(this.factStore.factsIndex.values())
.filter(f => f.id !== fact.id);
const conflicts = [];
for (const otherFact of allFacts) {
const conflictScore = this.detectConflict(fact, otherFact);
if (conflictScore > 0.5) {
conflicts.push({
...otherFact,
conflictScore,
});
}
}
return conflicts.sort((a, b) => b.conflictScore - a.conflictScore);
}
async findDependencyFacts(fact) {
const allFacts = Array.from(this.factStore.factsIndex.values())
.filter(f => f.id !== fact.id);
const dependencies = [];
for (const otherFact of allFacts) {
const dependency = this.detectDependency(fact, otherFact);
if (dependency) {
dependencies.push({
...otherFact,
...dependency,
});
}
}
return dependencies.sort((a, b) => b.dependencyScore - a.dependencyScore);
}
calculateContentSimilarity(fact1, fact2) {
const content1 = fact1.content.toLowerCase();
const content2 = fact2.content.toLowerCase();
let score = 0;
if (fact1.type === fact2.type) {
score += 0.2;
}
if (fact1.domain === fact2.domain) {
score += 0.2;
}
const commonTags = fact1.tags.filter(tag => fact2.tags.includes(tag));
score += (commonTags.length / Math.max(fact1.tags.length, fact2.tags.length)) * 0.3;
const words1 = new Set(content1.split(/\s+/));
const words2 = new Set(content2.split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
if (union.size > 0) {
score += (intersection.size / union.size) * 0.3;
}
return Math.min(1, score);
}
detectConflict(fact1, fact2) {
const content1 = fact1.content.toLowerCase();
const content2 = fact2.content.toLowerCase();
let conflictScore = 0;
if (fact1.type === 'verified_pattern' && fact2.type === 'anti_pattern') {
const words1 = new Set(content1.split(/\s+/));
const words2 = new Set(content2.split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
if (intersection.size > 3) {
conflictScore += 0.7;
}
}
const conflictTerms = [
['use', 'avoid'], ['do', "don't"], ['should', 'never'],
['recommended', 'discouraged'], ['best', 'worst'],
['good', 'bad'], ['correct', 'incorrect']
];
for (const [positive, negative] of conflictTerms) {
if ((content1.includes(positive) && content2.includes(negative)) ||
(content1.includes(negative) && content2.includes(positive))) {
conflictScore += 0.3;
}
}
if (fact1.domain === fact2.domain && fact1.type !== fact2.type) {
conflictScore += 0.1;
}
return Math.min(1, conflictScore);
}
detectDependency(fact1, fact2) {
const content1 = fact1.content.toLowerCase();
const content2 = fact2.content.toLowerCase();
if (fact1.type === 'optimization' && fact2.type === 'verified_pattern') {
const words1 = new Set(content1.split(/\s+/));
const words2 = new Set(content2.split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
if (intersection.size > 2) {
return {
relationType: 'extends',
dependencyScore: 0.7,
reason: 'Optimization builds on verified pattern',
};
}
}
if (fact1.type === 'debugging_solution' && fact2.type === 'anti_pattern') {
const words1 = new Set(content1.split(/\s+/));
const words2 = new Set(content2.split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
if (intersection.size > 2) {
return {
relationType: 'prevents',
dependencyScore: 0.8,
reason: 'Solution prevents anti-pattern',
};
}
}
const requirementPatterns = [
/requires?\s+([^.]+)/gi,
/depends?\s+on\s+([^.]+)/gi,
/needs?\s+([^.]+)/gi,
/prerequisite[:\s]+([^.]+)/gi,
];
for (const pattern of requirementPatterns) {
const matches = [...content1.matchAll(pattern)];
for (const match of matches) {
const requirement = match[1].toLowerCase();
if (content2.includes(requirement)) {
return {
relationType: 'requires',
dependencyScore: 0.6,
reason: 'Explicit dependency mentioned',
};
}
}
}
return null;
}
isValidRelationshipType(type) {
return this.relationshipTypes.hasOwnProperty(type);
}
getRelationshipTypes() {
return Object.values(this.relationshipTypes);
}
async getFactRelationships(factId, includeInverse = true) {
const fact = this.factStore.factsIndex.get(factId);
if (!fact) {
throw new Error(`Fact ${factId} not found`);
}
const relationships = fact.relationships || [];
if (!includeInverse) {
return relationships;
}
const allRelationships = [...relationships];
const allFacts = Array.from(this.factStore.factsIndex.values());
for (const otherFact of allFacts) {
if (otherFact.id === factId || !otherFact.relationships) continue;
for (const rel of otherFact.relationships) {
if (rel.targetId === factId) {
allRelationships.push({
...rel,
sourceId: otherFact.id,
isInverse: true,
});
}
}
}
return allRelationships;
}
async getRelationshipStats() {
const allFacts = Array.from(this.factStore.factsIndex.values());
let totalRelationships = 0;
const typeCount = {};
for (const fact of allFacts) {
if (fact.relationships) {
totalRelationships += fact.relationships.length;
for (const rel of fact.relationships) {
typeCount[rel.type] = (typeCount[rel.type] || 0) + 1;
}
}
}
return {
totalRelationships,
averageRelationshipsPerFact: allFacts.length > 0 ? totalRelationships / allFacts.length : 0,
relationshipsByType: typeCount,
factsWithRelationships: allFacts.filter(f => f.relationships && f.relationships.length > 0).length,
};
}
}