claude-brain 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/VERSION +1 -0
- package/assets/CLAUDE.md +307 -0
- package/bunfig.toml +8 -0
- package/package.json +74 -0
- package/src/automation/auto-context.ts +240 -0
- package/src/automation/decision-detector.ts +452 -0
- package/src/automation/index.ts +11 -0
- package/src/automation/proactive-recall.ts +373 -0
- package/src/automation/project-detector.ts +297 -0
- package/src/cli/auto-setup.ts +74 -0
- package/src/cli/bin.ts +110 -0
- package/src/cli/commands/install-mcp.ts +50 -0
- package/src/cli/commands/serve.ts +129 -0
- package/src/cli/diagnose.ts +4 -0
- package/src/cli/health-check.ts +4 -0
- package/src/cli/migrate-chroma.ts +106 -0
- package/src/cli/setup.ts +4 -0
- package/src/config/defaults.ts +47 -0
- package/src/config/home.ts +55 -0
- package/src/config/index.ts +7 -0
- package/src/config/loader.ts +166 -0
- package/src/config/migration.ts +76 -0
- package/src/config/schema.ts +257 -0
- package/src/config/validator.ts +184 -0
- package/src/config/watcher.ts +86 -0
- package/src/context/assembler.ts +398 -0
- package/src/context/cache-manager.ts +101 -0
- package/src/context/formatter.ts +84 -0
- package/src/context/hierarchy.ts +85 -0
- package/src/context/index.ts +83 -0
- package/src/context/progress-tracker.ts +174 -0
- package/src/context/standards-manager.ts +267 -0
- package/src/context/types.ts +252 -0
- package/src/context/validator.ts +58 -0
- package/src/cross-project/affinity.ts +162 -0
- package/src/cross-project/generalizer.ts +283 -0
- package/src/cross-project/index.ts +13 -0
- package/src/cross-project/transfer.ts +201 -0
- package/src/diagnostics/index.ts +123 -0
- package/src/health/index.ts +229 -0
- package/src/index.ts +7 -0
- package/src/knowledge/entity-extractor.ts +416 -0
- package/src/knowledge/graph/builder.ts +159 -0
- package/src/knowledge/graph/linker.ts +201 -0
- package/src/knowledge/graph/memory-graph.ts +359 -0
- package/src/knowledge/graph/schema.ts +99 -0
- package/src/knowledge/graph/search.ts +168 -0
- package/src/knowledge/relationship-extractor.ts +108 -0
- package/src/memory/chroma/client.ts +169 -0
- package/src/memory/chroma/collection-manager.ts +94 -0
- package/src/memory/chroma/config.ts +46 -0
- package/src/memory/chroma/embeddings.ts +153 -0
- package/src/memory/chroma/index.ts +82 -0
- package/src/memory/chroma/migration.ts +270 -0
- package/src/memory/chroma/schemas.ts +69 -0
- package/src/memory/chroma/search.ts +315 -0
- package/src/memory/chroma/store.ts +694 -0
- package/src/memory/consolidation/archiver.ts +164 -0
- package/src/memory/consolidation/merger.ts +186 -0
- package/src/memory/consolidation/scorer.ts +138 -0
- package/src/memory/context-builder.ts +236 -0
- package/src/memory/database.ts +169 -0
- package/src/memory/embedding-utils.ts +156 -0
- package/src/memory/embeddings.ts +226 -0
- package/src/memory/episodic/detector.ts +108 -0
- package/src/memory/episodic/manager.ts +334 -0
- package/src/memory/episodic/summarizer.ts +179 -0
- package/src/memory/episodic/types.ts +52 -0
- package/src/memory/index.ts +395 -0
- package/src/memory/knowledge-extractor.ts +455 -0
- package/src/memory/learning.ts +378 -0
- package/src/memory/patterns.ts +396 -0
- package/src/memory/schema.ts +56 -0
- package/src/memory/search.ts +309 -0
- package/src/memory/store.ts +344 -0
- package/src/memory/types.ts +121 -0
- package/src/optimization/index.ts +10 -0
- package/src/optimization/precompute.ts +202 -0
- package/src/optimization/semantic-cache.ts +207 -0
- package/src/orchestrator/coordinator.ts +272 -0
- package/src/orchestrator/decision-logger.ts +228 -0
- package/src/orchestrator/event-emitter.ts +198 -0
- package/src/orchestrator/event-queue.ts +184 -0
- package/src/orchestrator/handlers/base-handler.ts +70 -0
- package/src/orchestrator/handlers/context-handler.ts +73 -0
- package/src/orchestrator/handlers/decision-handler.ts +204 -0
- package/src/orchestrator/handlers/index.ts +10 -0
- package/src/orchestrator/handlers/status-handler.ts +131 -0
- package/src/orchestrator/handlers/task-handler.ts +171 -0
- package/src/orchestrator/index.ts +275 -0
- package/src/orchestrator/task-parser.ts +284 -0
- package/src/orchestrator/types.ts +98 -0
- package/src/phase12/index.ts +456 -0
- package/src/prediction/context-anticipator.ts +198 -0
- package/src/prediction/decision-predictor.ts +184 -0
- package/src/prediction/index.ts +13 -0
- package/src/prediction/recommender.ts +268 -0
- package/src/reasoning/chain-retrieval.ts +247 -0
- package/src/reasoning/counterfactual.ts +248 -0
- package/src/reasoning/index.ts +13 -0
- package/src/reasoning/synthesizer.ts +169 -0
- package/src/retrieval/bm25/index.ts +300 -0
- package/src/retrieval/bm25/tokenizer.ts +184 -0
- package/src/retrieval/feedback/adaptive.ts +223 -0
- package/src/retrieval/feedback/index.ts +16 -0
- package/src/retrieval/feedback/metrics.ts +223 -0
- package/src/retrieval/feedback/store.ts +283 -0
- package/src/retrieval/fusion/index.ts +194 -0
- package/src/retrieval/fusion/rrf.ts +163 -0
- package/src/retrieval/index.ts +12 -0
- package/src/retrieval/pipeline.ts +375 -0
- package/src/retrieval/query/expander.ts +198 -0
- package/src/retrieval/query/index.ts +27 -0
- package/src/retrieval/query/intent-classifier.ts +236 -0
- package/src/retrieval/query/temporal-parser.ts +295 -0
- package/src/retrieval/reranker/index.ts +188 -0
- package/src/retrieval/reranker/model.ts +95 -0
- package/src/retrieval/service.ts +125 -0
- package/src/retrieval/types.ts +162 -0
- package/src/scripts/health-check.ts +118 -0
- package/src/scripts/setup.ts +122 -0
- package/src/server/handlers/call-tool.ts +194 -0
- package/src/server/handlers/index.ts +9 -0
- package/src/server/handlers/list-tools.ts +18 -0
- package/src/server/handlers/tools/analyze-decision-evolution.ts +71 -0
- package/src/server/handlers/tools/auto-remember.ts +200 -0
- package/src/server/handlers/tools/create-project.ts +135 -0
- package/src/server/handlers/tools/detect-trends.ts +80 -0
- package/src/server/handlers/tools/find-cross-project-patterns.ts +73 -0
- package/src/server/handlers/tools/get-activity-log.ts +194 -0
- package/src/server/handlers/tools/get-code-standards.ts +124 -0
- package/src/server/handlers/tools/get-corrections.ts +154 -0
- package/src/server/handlers/tools/get-decision-timeline.ts +86 -0
- package/src/server/handlers/tools/get-episode.ts +93 -0
- package/src/server/handlers/tools/get-patterns.ts +158 -0
- package/src/server/handlers/tools/get-phase12-status.ts +63 -0
- package/src/server/handlers/tools/get-project-context.ts +75 -0
- package/src/server/handlers/tools/get-recommendations.ts +65 -0
- package/src/server/handlers/tools/index.ts +33 -0
- package/src/server/handlers/tools/init-project.ts +710 -0
- package/src/server/handlers/tools/list-episodes.ts +80 -0
- package/src/server/handlers/tools/list-projects.ts +125 -0
- package/src/server/handlers/tools/rate-memory.ts +95 -0
- package/src/server/handlers/tools/recall-similar.ts +87 -0
- package/src/server/handlers/tools/recognize-pattern.ts +126 -0
- package/src/server/handlers/tools/record-correction.ts +125 -0
- package/src/server/handlers/tools/remember-decision.ts +153 -0
- package/src/server/handlers/tools/schemas.ts +241 -0
- package/src/server/handlers/tools/search-knowledge-graph.ts +89 -0
- package/src/server/handlers/tools/smart-context.ts +124 -0
- package/src/server/handlers/tools/update-progress.ts +114 -0
- package/src/server/handlers/tools/what-if-analysis.ts +73 -0
- package/src/server/http-api.ts +474 -0
- package/src/server/index.ts +40 -0
- package/src/server/mcp-server.ts +283 -0
- package/src/server/providers/index.ts +7 -0
- package/src/server/providers/prompts.ts +327 -0
- package/src/server/providers/resources.ts +427 -0
- package/src/server/services.ts +388 -0
- package/src/server/types.ts +39 -0
- package/src/server/utils/error-handler.ts +155 -0
- package/src/server/utils/index.ts +13 -0
- package/src/server/utils/memory-indicator.ts +83 -0
- package/src/server/utils/request-context.ts +122 -0
- package/src/server/utils/response-formatter.ts +124 -0
- package/src/server/utils/validators.ts +210 -0
- package/src/setup/index.ts +22 -0
- package/src/setup/wizard.ts +321 -0
- package/src/temporal/evolution.ts +197 -0
- package/src/temporal/index.ts +16 -0
- package/src/temporal/query-processor.ts +190 -0
- package/src/temporal/timeline.ts +259 -0
- package/src/temporal/trends.ts +263 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/registry.ts +106 -0
- package/src/tools/schemas.test.ts +30 -0
- package/src/tools/schemas.ts +907 -0
- package/src/tools/types.ts +412 -0
- package/src/utils/circuit-breaker.ts +130 -0
- package/src/utils/cleanup.ts +34 -0
- package/src/utils/error-handler.ts +132 -0
- package/src/utils/error-messages.ts +60 -0
- package/src/utils/fallback.ts +45 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/logger-utils.ts +80 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/phase12-helper.ts +56 -0
- package/src/utils/retry.ts +94 -0
- package/src/utils/transaction.ts +63 -0
- package/src/vault/frontmatter.ts +264 -0
- package/src/vault/index.ts +318 -0
- package/src/vault/paths.ts +106 -0
- package/src/vault/query.ts +422 -0
- package/src/vault/reader.ts +264 -0
- package/src/vault/templates.ts +186 -0
- package/src/vault/types.ts +73 -0
- package/src/vault/watcher.ts +277 -0
- package/src/vault/writer.ts +393 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Builder
|
|
3
|
+
* Auto-populates the graph when decisions/patterns/corrections are stored
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { InMemoryKnowledgeGraph } from './memory-graph'
|
|
8
|
+
import { EntityExtractor, type ExtractedEntity } from '../entity-extractor'
|
|
9
|
+
import { RelationshipExtractor } from '../relationship-extractor'
|
|
10
|
+
import type { StoreDecisionInput, StoredDecision } from '../../memory/chroma/store'
|
|
11
|
+
import type { CollectionManager } from '../../memory/chroma/collection-manager'
|
|
12
|
+
|
|
13
|
+
export class KnowledgeGraphBuilder {
|
|
14
|
+
private graph: InMemoryKnowledgeGraph
|
|
15
|
+
private entityExtractor: EntityExtractor
|
|
16
|
+
private relationshipExtractor: RelationshipExtractor
|
|
17
|
+
private logger: Logger
|
|
18
|
+
|
|
19
|
+
constructor(graph: InMemoryKnowledgeGraph, logger: Logger) {
|
|
20
|
+
this.graph = graph
|
|
21
|
+
this.entityExtractor = new EntityExtractor()
|
|
22
|
+
this.relationshipExtractor = new RelationshipExtractor()
|
|
23
|
+
this.logger = logger.child({ component: 'graph-builder' })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async initialize(): Promise<void> {
|
|
27
|
+
await this.entityExtractor.initialize()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
processDecision(input: StoreDecisionInput & { id: string }): void {
|
|
31
|
+
try {
|
|
32
|
+
const fullText = `${input.decision} ${input.context} ${input.reasoning}`
|
|
33
|
+
const entities = this.entityExtractor.extract(fullText)
|
|
34
|
+
const relationships = this.relationshipExtractor.extract(fullText, entities)
|
|
35
|
+
|
|
36
|
+
// Create decision node
|
|
37
|
+
const decisionNode = this.graph.addNode({
|
|
38
|
+
name: input.decision.slice(0, 100),
|
|
39
|
+
type: 'decision',
|
|
40
|
+
properties: {
|
|
41
|
+
decision_id: input.id,
|
|
42
|
+
project: input.project,
|
|
43
|
+
context: input.context,
|
|
44
|
+
reasoning: input.reasoning,
|
|
45
|
+
full_decision: input.decision
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Create project node if it doesn't exist
|
|
50
|
+
let projectNode = this.graph.findNodeByName(input.project, 'project')
|
|
51
|
+
if (!projectNode) {
|
|
52
|
+
projectNode = this.graph.addNode({
|
|
53
|
+
name: input.project,
|
|
54
|
+
type: 'project',
|
|
55
|
+
properties: {}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Link decision to project
|
|
60
|
+
this.graph.addEdge({
|
|
61
|
+
source: decisionNode.id,
|
|
62
|
+
target: projectNode.id,
|
|
63
|
+
relationship: 'decided_in',
|
|
64
|
+
weight: 1.0,
|
|
65
|
+
properties: {}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Create entity nodes and link to decision
|
|
69
|
+
const entityNodeMap = new Map<string, string>() // normalizedName → nodeId
|
|
70
|
+
for (const entity of entities) {
|
|
71
|
+
let entityNode = this.graph.findNodeByName(entity.normalizedName, entity.type)
|
|
72
|
+
if (!entityNode) {
|
|
73
|
+
entityNode = this.graph.addNode({
|
|
74
|
+
name: entity.normalizedName,
|
|
75
|
+
type: entity.type,
|
|
76
|
+
properties: {
|
|
77
|
+
confidence: entity.confidence,
|
|
78
|
+
source: entity.source
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
entityNodeMap.set(entity.normalizedName, entityNode.id)
|
|
83
|
+
|
|
84
|
+
// Link decision to entity
|
|
85
|
+
this.graph.addEdge({
|
|
86
|
+
source: decisionNode.id,
|
|
87
|
+
target: entityNode.id,
|
|
88
|
+
relationship: 'relates_to',
|
|
89
|
+
weight: entity.confidence,
|
|
90
|
+
properties: {}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create relationship edges between entities
|
|
95
|
+
for (const rel of relationships) {
|
|
96
|
+
const sourceId = entityNodeMap.get(rel.sourceEntity)
|
|
97
|
+
const targetId = entityNodeMap.get(rel.targetEntity)
|
|
98
|
+
|
|
99
|
+
if (sourceId && targetId) {
|
|
100
|
+
this.graph.addEdge({
|
|
101
|
+
source: sourceId,
|
|
102
|
+
target: targetId,
|
|
103
|
+
relationship: rel.relationship,
|
|
104
|
+
weight: rel.confidence,
|
|
105
|
+
properties: { evidence: rel.evidence }
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.logger.debug({
|
|
111
|
+
decisionId: input.id,
|
|
112
|
+
entities: entities.length,
|
|
113
|
+
relationships: relationships.length
|
|
114
|
+
}, 'Decision processed into knowledge graph')
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.logger.error({ error, decisionId: input.id }, 'Failed to process decision into graph')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async migrateExistingDecisions(collections: CollectionManager): Promise<{ processed: number; errors: number }> {
|
|
121
|
+
let processed = 0
|
|
122
|
+
let errors = 0
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const collection = await collections.getDecisions()
|
|
126
|
+
const result = await collection.get({
|
|
127
|
+
include: ['documents', 'metadatas']
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < result.ids.length; i++) {
|
|
131
|
+
try {
|
|
132
|
+
const id = result.ids[i]
|
|
133
|
+
const document = result.documents?.[i] as string
|
|
134
|
+
const metadata = result.metadatas?.[i] as Record<string, any>
|
|
135
|
+
|
|
136
|
+
if (!document || !metadata) continue
|
|
137
|
+
|
|
138
|
+
this.processDecision({
|
|
139
|
+
id,
|
|
140
|
+
project: metadata.project || 'unknown',
|
|
141
|
+
context: metadata.context || '',
|
|
142
|
+
decision: document,
|
|
143
|
+
reasoning: metadata.reasoning || ''
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
processed++
|
|
147
|
+
} catch {
|
|
148
|
+
errors++
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.logger.info({ processed, errors }, 'Migration of existing decisions complete')
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.error({ error }, 'Failed to migrate existing decisions')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { processed, errors }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Reference Linker
|
|
3
|
+
* Finds implicit links, contradictions, and decision chains
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { InMemoryKnowledgeGraph } from './memory-graph'
|
|
8
|
+
import type { GraphNode, GraphEdge } from './schema'
|
|
9
|
+
|
|
10
|
+
export interface ImplicitLink {
|
|
11
|
+
sourceId: string
|
|
12
|
+
targetId: string
|
|
13
|
+
sourceName: string
|
|
14
|
+
targetName: string
|
|
15
|
+
reason: string
|
|
16
|
+
confidence: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Contradiction {
|
|
20
|
+
decisionA: GraphNode
|
|
21
|
+
decisionB: GraphNode
|
|
22
|
+
reason: string
|
|
23
|
+
confidence: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DecisionChain {
|
|
27
|
+
topic: string
|
|
28
|
+
decisions: GraphNode[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class CrossReferenceLinker {
|
|
32
|
+
private graph: InMemoryKnowledgeGraph
|
|
33
|
+
private logger: Logger
|
|
34
|
+
|
|
35
|
+
constructor(graph: InMemoryKnowledgeGraph, logger: Logger) {
|
|
36
|
+
this.graph = graph
|
|
37
|
+
this.logger = logger.child({ component: 'cross-reference-linker' })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
findImplicitLinks(): ImplicitLink[] {
|
|
41
|
+
const links: ImplicitLink[] = []
|
|
42
|
+
const decisionNodes = this.graph.findNodes({ type: 'decision' })
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < decisionNodes.length; i++) {
|
|
45
|
+
for (let j = i + 1; j < decisionNodes.length; j++) {
|
|
46
|
+
const nodeA = decisionNodes[i]
|
|
47
|
+
const nodeB = decisionNodes[j]
|
|
48
|
+
|
|
49
|
+
// Check if they share entities (connected via common neighbor)
|
|
50
|
+
const sharedEntities = this.findSharedEntities(nodeA.id, nodeB.id)
|
|
51
|
+
|
|
52
|
+
if (sharedEntities.length >= 2) {
|
|
53
|
+
// Already directly connected?
|
|
54
|
+
const existingEdges = this.graph.getEdges(nodeA.id, 'outgoing')
|
|
55
|
+
const alreadyLinked = existingEdges.some(e => e.target === nodeB.id && e.relationship === 'similar_to')
|
|
56
|
+
|
|
57
|
+
if (!alreadyLinked) {
|
|
58
|
+
links.push({
|
|
59
|
+
sourceId: nodeA.id,
|
|
60
|
+
targetId: nodeB.id,
|
|
61
|
+
sourceName: nodeA.name,
|
|
62
|
+
targetName: nodeB.name,
|
|
63
|
+
reason: `Share ${sharedEntities.length} entities: ${sharedEntities.slice(0, 3).join(', ')}`,
|
|
64
|
+
confidence: Math.min(0.9, 0.5 + sharedEntities.length * 0.1)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.logger.debug({ linkCount: links.length }, 'Found implicit links')
|
|
72
|
+
return links
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
findContradictions(): Contradiction[] {
|
|
76
|
+
const contradictions: Contradiction[] = []
|
|
77
|
+
const decisionNodes = this.graph.findNodes({ type: 'decision' })
|
|
78
|
+
|
|
79
|
+
const contradictionKeywords = ['not', 'avoid', 'don\'t', 'never', 'instead', 'rather than', 'rejected']
|
|
80
|
+
const approvalKeywords = ['chose', 'use', 'adopt', 'selected', 'prefer', 'recommend']
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < decisionNodes.length; i++) {
|
|
83
|
+
for (let j = i + 1; j < decisionNodes.length; j++) {
|
|
84
|
+
const nodeA = decisionNodes[i]
|
|
85
|
+
const nodeB = decisionNodes[j]
|
|
86
|
+
|
|
87
|
+
// Check if same project
|
|
88
|
+
const projectA = nodeA.properties.project as string
|
|
89
|
+
const projectB = nodeB.properties.project as string
|
|
90
|
+
if (projectA !== projectB) continue
|
|
91
|
+
|
|
92
|
+
// Check if they share entities but with opposing sentiment
|
|
93
|
+
const sharedEntities = this.findSharedEntities(nodeA.id, nodeB.id)
|
|
94
|
+
if (sharedEntities.length === 0) continue
|
|
95
|
+
|
|
96
|
+
const textA = (nodeA.properties.full_decision as string || nodeA.name).toLowerCase()
|
|
97
|
+
const textB = (nodeB.properties.full_decision as string || nodeB.name).toLowerCase()
|
|
98
|
+
|
|
99
|
+
// Check for opposing sentiment
|
|
100
|
+
const aNegative = contradictionKeywords.some(k => textA.includes(k))
|
|
101
|
+
const bNegative = contradictionKeywords.some(k => textB.includes(k))
|
|
102
|
+
const aPositive = approvalKeywords.some(k => textA.includes(k))
|
|
103
|
+
const bPositive = approvalKeywords.some(k => textB.includes(k))
|
|
104
|
+
|
|
105
|
+
if ((aNegative && bPositive) || (aPositive && bNegative)) {
|
|
106
|
+
contradictions.push({
|
|
107
|
+
decisionA: nodeA,
|
|
108
|
+
decisionB: nodeB,
|
|
109
|
+
reason: `Opposing sentiment about shared entities: ${sharedEntities.join(', ')}`,
|
|
110
|
+
confidence: 0.7
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.logger.debug({ count: contradictions.length }, 'Found potential contradictions')
|
|
117
|
+
return contradictions
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
findDecisionChains(): DecisionChain[] {
|
|
121
|
+
const chains: DecisionChain[] = []
|
|
122
|
+
const decisionNodes = this.graph.findNodes({ type: 'decision' })
|
|
123
|
+
|
|
124
|
+
// Group by shared topics (entity neighbors)
|
|
125
|
+
const topicGroups = new Map<string, GraphNode[]>()
|
|
126
|
+
|
|
127
|
+
for (const decision of decisionNodes) {
|
|
128
|
+
const edges = this.graph.getEdges(decision.id, 'outgoing')
|
|
129
|
+
for (const edge of edges) {
|
|
130
|
+
if (edge.relationship === 'relates_to') {
|
|
131
|
+
const target = this.graph.getNode(edge.target)
|
|
132
|
+
if (target && target.type === 'technology') {
|
|
133
|
+
const topic = target.name
|
|
134
|
+
if (!topicGroups.has(topic)) {
|
|
135
|
+
topicGroups.set(topic, [])
|
|
136
|
+
}
|
|
137
|
+
topicGroups.get(topic)!.push(decision)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create chains from groups with 2+ decisions
|
|
144
|
+
for (const [topic, decisions] of topicGroups) {
|
|
145
|
+
if (decisions.length >= 2) {
|
|
146
|
+
// Sort by creation time
|
|
147
|
+
const sorted = [...decisions].sort((a, b) =>
|
|
148
|
+
a.created_at.localeCompare(b.created_at)
|
|
149
|
+
)
|
|
150
|
+
chains.push({ topic, decisions: sorted })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.logger.debug({ chainCount: chains.length }, 'Found decision chains')
|
|
155
|
+
return chains
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
applyImplicitLinks(links: ImplicitLink[]): number {
|
|
159
|
+
let applied = 0
|
|
160
|
+
for (const link of links) {
|
|
161
|
+
try {
|
|
162
|
+
this.graph.addEdge({
|
|
163
|
+
source: link.sourceId,
|
|
164
|
+
target: link.targetId,
|
|
165
|
+
relationship: 'similar_to',
|
|
166
|
+
weight: link.confidence,
|
|
167
|
+
properties: { reason: link.reason, auto_linked: true }
|
|
168
|
+
})
|
|
169
|
+
applied++
|
|
170
|
+
} catch {
|
|
171
|
+
// May fail if nodes were deleted
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return applied
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private findSharedEntities(nodeAId: string, nodeBId: string): string[] {
|
|
178
|
+
const edgesA = this.graph.getEdges(nodeAId, 'outgoing')
|
|
179
|
+
const edgesB = this.graph.getEdges(nodeBId, 'outgoing')
|
|
180
|
+
|
|
181
|
+
const neighborsA = new Set<string>()
|
|
182
|
+
for (const edge of edgesA) {
|
|
183
|
+
if (edge.relationship === 'relates_to') {
|
|
184
|
+
const target = this.graph.getNode(edge.target)
|
|
185
|
+
if (target) neighborsA.add(target.name)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const shared: string[] = []
|
|
190
|
+
for (const edge of edgesB) {
|
|
191
|
+
if (edge.relationship === 'relates_to') {
|
|
192
|
+
const target = this.graph.getNode(edge.target)
|
|
193
|
+
if (target && neighborsA.has(target.name)) {
|
|
194
|
+
shared.push(target.name)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return shared
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Knowledge Graph
|
|
3
|
+
* Graph data structure with traversal, persistence, and search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto'
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
8
|
+
import { dirname } from 'path'
|
|
9
|
+
import type {
|
|
10
|
+
GraphNode,
|
|
11
|
+
GraphEdge,
|
|
12
|
+
KnowledgeGraph,
|
|
13
|
+
TraversalOptions,
|
|
14
|
+
EntityType,
|
|
15
|
+
SerializedGraph
|
|
16
|
+
} from './schema'
|
|
17
|
+
import type { Logger } from 'pino'
|
|
18
|
+
|
|
19
|
+
export class InMemoryKnowledgeGraph implements KnowledgeGraph {
|
|
20
|
+
private nodes: Map<string, GraphNode> = new Map()
|
|
21
|
+
private edges: Map<string, GraphEdge> = new Map()
|
|
22
|
+
private outgoing: Map<string, Set<string>> = new Map()
|
|
23
|
+
private incoming: Map<string, Set<string>> = new Map()
|
|
24
|
+
private logger: Logger
|
|
25
|
+
private autoSaveTimer: ReturnType<typeof setInterval> | null = null
|
|
26
|
+
private dirty = false
|
|
27
|
+
|
|
28
|
+
constructor(logger: Logger) {
|
|
29
|
+
this.logger = logger.child({ component: 'knowledge-graph' })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addNode(input: Omit<GraphNode, 'id' | 'created_at' | 'updated_at'>): GraphNode {
|
|
33
|
+
const now = new Date().toISOString()
|
|
34
|
+
const node: GraphNode = {
|
|
35
|
+
...input,
|
|
36
|
+
id: randomUUID(),
|
|
37
|
+
created_at: now,
|
|
38
|
+
updated_at: now
|
|
39
|
+
}
|
|
40
|
+
this.nodes.set(node.id, node)
|
|
41
|
+
this.outgoing.set(node.id, new Set())
|
|
42
|
+
this.incoming.set(node.id, new Set())
|
|
43
|
+
this.dirty = true
|
|
44
|
+
return node
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getNode(id: string): GraphNode | undefined {
|
|
48
|
+
return this.nodes.get(id)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
updateNode(id: string, updates: Partial<Omit<GraphNode, 'id' | 'created_at'>>): GraphNode | undefined {
|
|
52
|
+
const node = this.nodes.get(id)
|
|
53
|
+
if (!node) return undefined
|
|
54
|
+
|
|
55
|
+
const updated: GraphNode = {
|
|
56
|
+
...node,
|
|
57
|
+
...updates,
|
|
58
|
+
id: node.id,
|
|
59
|
+
created_at: node.created_at,
|
|
60
|
+
updated_at: new Date().toISOString()
|
|
61
|
+
}
|
|
62
|
+
this.nodes.set(id, updated)
|
|
63
|
+
this.dirty = true
|
|
64
|
+
return updated
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deleteNode(id: string): boolean {
|
|
68
|
+
if (!this.nodes.has(id)) return false
|
|
69
|
+
|
|
70
|
+
// Delete all connected edges
|
|
71
|
+
const outEdges = this.outgoing.get(id) || new Set()
|
|
72
|
+
const inEdges = this.incoming.get(id) || new Set()
|
|
73
|
+
|
|
74
|
+
for (const edgeId of outEdges) {
|
|
75
|
+
const edge = this.edges.get(edgeId)
|
|
76
|
+
if (edge) {
|
|
77
|
+
this.incoming.get(edge.target)?.delete(edgeId)
|
|
78
|
+
this.edges.delete(edgeId)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const edgeId of inEdges) {
|
|
83
|
+
const edge = this.edges.get(edgeId)
|
|
84
|
+
if (edge) {
|
|
85
|
+
this.outgoing.get(edge.source)?.delete(edgeId)
|
|
86
|
+
this.edges.delete(edgeId)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.outgoing.delete(id)
|
|
91
|
+
this.incoming.delete(id)
|
|
92
|
+
this.nodes.delete(id)
|
|
93
|
+
this.dirty = true
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
addEdge(input: Omit<GraphEdge, 'id' | 'created_at'>): GraphEdge {
|
|
98
|
+
if (!this.nodes.has(input.source) || !this.nodes.has(input.target)) {
|
|
99
|
+
throw new Error(`Both source (${input.source}) and target (${input.target}) must exist`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const edge: GraphEdge = {
|
|
103
|
+
...input,
|
|
104
|
+
id: randomUUID(),
|
|
105
|
+
created_at: new Date().toISOString()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.edges.set(edge.id, edge)
|
|
109
|
+
|
|
110
|
+
if (!this.outgoing.has(input.source)) this.outgoing.set(input.source, new Set())
|
|
111
|
+
if (!this.incoming.has(input.target)) this.incoming.set(input.target, new Set())
|
|
112
|
+
|
|
113
|
+
this.outgoing.get(input.source)!.add(edge.id)
|
|
114
|
+
this.incoming.get(input.target)!.add(edge.id)
|
|
115
|
+
this.dirty = true
|
|
116
|
+
return edge
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getEdges(nodeId: string, direction: 'outgoing' | 'incoming' | 'both' = 'both'): GraphEdge[] {
|
|
120
|
+
const result: GraphEdge[] = []
|
|
121
|
+
|
|
122
|
+
if (direction === 'outgoing' || direction === 'both') {
|
|
123
|
+
const outEdgeIds = this.outgoing.get(nodeId) || new Set()
|
|
124
|
+
for (const edgeId of outEdgeIds) {
|
|
125
|
+
const edge = this.edges.get(edgeId)
|
|
126
|
+
if (edge) result.push(edge)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (direction === 'incoming' || direction === 'both') {
|
|
131
|
+
const inEdgeIds = this.incoming.get(nodeId) || new Set()
|
|
132
|
+
for (const edgeId of inEdgeIds) {
|
|
133
|
+
const edge = this.edges.get(edgeId)
|
|
134
|
+
if (edge) result.push(edge)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
deleteEdge(id: string): boolean {
|
|
142
|
+
const edge = this.edges.get(id)
|
|
143
|
+
if (!edge) return false
|
|
144
|
+
|
|
145
|
+
this.outgoing.get(edge.source)?.delete(id)
|
|
146
|
+
this.incoming.get(edge.target)?.delete(id)
|
|
147
|
+
this.edges.delete(id)
|
|
148
|
+
this.dirty = true
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
traverse(startId: string, options: TraversalOptions = {}): GraphNode[] {
|
|
153
|
+
const { maxDepth = 3, nodeTypes, relationshipTypes, limit = 100 } = options
|
|
154
|
+
|
|
155
|
+
if (!this.nodes.has(startId)) return []
|
|
156
|
+
|
|
157
|
+
const visited = new Set<string>()
|
|
158
|
+
const result: GraphNode[] = []
|
|
159
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }]
|
|
160
|
+
|
|
161
|
+
while (queue.length > 0 && result.length < limit) {
|
|
162
|
+
const { id, depth } = queue.shift()!
|
|
163
|
+
|
|
164
|
+
if (visited.has(id)) continue
|
|
165
|
+
visited.add(id)
|
|
166
|
+
|
|
167
|
+
const node = this.nodes.get(id)
|
|
168
|
+
if (!node) continue
|
|
169
|
+
|
|
170
|
+
if (nodeTypes && !nodeTypes.includes(node.type)) {
|
|
171
|
+
// Don't add to results but still traverse through
|
|
172
|
+
} else {
|
|
173
|
+
result.push(node)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (depth >= maxDepth) continue
|
|
177
|
+
|
|
178
|
+
// Get neighbors via edges
|
|
179
|
+
const edges = this.getEdges(id)
|
|
180
|
+
for (const edge of edges) {
|
|
181
|
+
if (relationshipTypes && !relationshipTypes.includes(edge.relationship)) continue
|
|
182
|
+
|
|
183
|
+
const neighborId = edge.source === id ? edge.target : edge.source
|
|
184
|
+
if (!visited.has(neighborId)) {
|
|
185
|
+
queue.push({ id: neighborId, depth: depth + 1 })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
shortestPath(sourceId: string, targetId: string): string[] | null {
|
|
194
|
+
if (!this.nodes.has(sourceId) || !this.nodes.has(targetId)) return null
|
|
195
|
+
if (sourceId === targetId) return [sourceId]
|
|
196
|
+
|
|
197
|
+
const visited = new Set<string>()
|
|
198
|
+
const parent = new Map<string, string>()
|
|
199
|
+
const queue: string[] = [sourceId]
|
|
200
|
+
visited.add(sourceId)
|
|
201
|
+
|
|
202
|
+
while (queue.length > 0) {
|
|
203
|
+
const current = queue.shift()!
|
|
204
|
+
|
|
205
|
+
const edges = this.getEdges(current)
|
|
206
|
+
for (const edge of edges) {
|
|
207
|
+
const neighbor = edge.source === current ? edge.target : edge.source
|
|
208
|
+
|
|
209
|
+
if (!visited.has(neighbor)) {
|
|
210
|
+
visited.add(neighbor)
|
|
211
|
+
parent.set(neighbor, current)
|
|
212
|
+
|
|
213
|
+
if (neighbor === targetId) {
|
|
214
|
+
// Reconstruct path
|
|
215
|
+
const path: string[] = [targetId]
|
|
216
|
+
let node = targetId
|
|
217
|
+
while (parent.has(node)) {
|
|
218
|
+
node = parent.get(node)!
|
|
219
|
+
path.unshift(node)
|
|
220
|
+
}
|
|
221
|
+
return path
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
queue.push(neighbor)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
findNodes(query: { type?: EntityType; name?: string; properties?: Record<string, unknown> }): GraphNode[] {
|
|
233
|
+
const results: GraphNode[] = []
|
|
234
|
+
|
|
235
|
+
for (const node of this.nodes.values()) {
|
|
236
|
+
if (query.type && node.type !== query.type) continue
|
|
237
|
+
if (query.name && !node.name.toLowerCase().includes(query.name.toLowerCase())) continue
|
|
238
|
+
|
|
239
|
+
if (query.properties) {
|
|
240
|
+
let match = true
|
|
241
|
+
for (const [key, value] of Object.entries(query.properties)) {
|
|
242
|
+
if (node.properties[key] !== value) {
|
|
243
|
+
match = false
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!match) continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
results.push(node)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return results
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
findNodeByName(name: string, type?: EntityType): GraphNode | undefined {
|
|
257
|
+
const lowerName = name.toLowerCase()
|
|
258
|
+
for (const node of this.nodes.values()) {
|
|
259
|
+
if (node.name.toLowerCase() === lowerName) {
|
|
260
|
+
if (type && node.type !== type) continue
|
|
261
|
+
return node
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
getNodeCount(): number {
|
|
268
|
+
return this.nodes.size
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getEdgeCount(): number {
|
|
272
|
+
return this.edges.size
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async save(path: string): Promise<void> {
|
|
276
|
+
const data: SerializedGraph = {
|
|
277
|
+
nodes: Array.from(this.nodes.values()),
|
|
278
|
+
edges: Array.from(this.edges.values()),
|
|
279
|
+
version: '1.0.0',
|
|
280
|
+
saved_at: new Date().toISOString()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await mkdir(dirname(path), { recursive: true })
|
|
285
|
+
await writeFile(path, JSON.stringify(data, null, 2), 'utf-8')
|
|
286
|
+
this.dirty = false
|
|
287
|
+
this.logger.debug({ path, nodes: data.nodes.length, edges: data.edges.length }, 'Graph saved')
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this.logger.error({ error, path }, 'Failed to save graph')
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async load(path: string): Promise<void> {
|
|
295
|
+
try {
|
|
296
|
+
const content = await readFile(path, 'utf-8')
|
|
297
|
+
const data: SerializedGraph = JSON.parse(content)
|
|
298
|
+
|
|
299
|
+
this.nodes.clear()
|
|
300
|
+
this.edges.clear()
|
|
301
|
+
this.outgoing.clear()
|
|
302
|
+
this.incoming.clear()
|
|
303
|
+
|
|
304
|
+
for (const node of data.nodes) {
|
|
305
|
+
this.nodes.set(node.id, node)
|
|
306
|
+
this.outgoing.set(node.id, new Set())
|
|
307
|
+
this.incoming.set(node.id, new Set())
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const edge of data.edges) {
|
|
311
|
+
this.edges.set(edge.id, edge)
|
|
312
|
+
this.outgoing.get(edge.source)?.add(edge.id)
|
|
313
|
+
this.incoming.get(edge.target)?.add(edge.id)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.dirty = false
|
|
317
|
+
this.logger.info({ path, nodes: data.nodes.length, edges: data.edges.length }, 'Graph loaded')
|
|
318
|
+
} catch (error: any) {
|
|
319
|
+
if (error.code === 'ENOENT') {
|
|
320
|
+
this.logger.info({ path }, 'No existing graph file, starting fresh')
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
this.logger.error({ error, path }, 'Failed to load graph')
|
|
324
|
+
throw error
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
startAutoSave(path: string, intervalMs: number = 60000): void {
|
|
329
|
+
this.stopAutoSave()
|
|
330
|
+
this.autoSaveTimer = setInterval(async () => {
|
|
331
|
+
if (this.dirty) {
|
|
332
|
+
try {
|
|
333
|
+
await this.save(path)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.logger.error({ error }, 'Auto-save failed')
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, intervalMs)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
stopAutoSave(): void {
|
|
342
|
+
if (this.autoSaveTimer) {
|
|
343
|
+
clearInterval(this.autoSaveTimer)
|
|
344
|
+
this.autoSaveTimer = null
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
isDirty(): boolean {
|
|
349
|
+
return this.dirty
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
clear(): void {
|
|
353
|
+
this.nodes.clear()
|
|
354
|
+
this.edges.clear()
|
|
355
|
+
this.outgoing.clear()
|
|
356
|
+
this.incoming.clear()
|
|
357
|
+
this.dirty = true
|
|
358
|
+
}
|
|
359
|
+
}
|