mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
645f31a06f
Phase 2 — Wedge 1: BrowserCausalAdapter
Exports ADR-122 Phase 2 selector-break events as a DD SparseMatrix per
origin. Off-diagonal weights are time-decayed adjacencies (half-life 24h
default). Includes role:name fuzzy-match keys for robust matching across
ref renames. graphId = `browser:causal:<origin>`.
Phase 3 — Wedges 3/6/7 (three adapters in parallel):
FederationTrustAdapter (ruflo-federation:trust-mesh)
Peer × peer with signature-confidence edges, stale-edge pruning
(default 7-day freshness window). Asymmetric (one-way trust).
CostAttributionAdapter (ruflo-cost-tracker:causation:<sessionId>)
Prompt → agent → MCP-call → model causation. L1-normalises costs per
parent so PR weights are proportional shares of blame.
ObservabilitySpanAdapter (ruflo-observability:trace:<traceId>)
Span dependency tree + cross-trace causedBy links. self-time-weighted
edges normalised per parent.
Phase 4 — Wedges 4/5:
KnowledgeGraphAdapter (ruflo-knowledge-graph:entities)
Entity-relation graph, multiple relations sum capped at 1.0 confidence.
RagMemoryAdapter (ruflo-rag-memory:chunks:<namespace>)
Chunk connectivity for personalized PR. Similarity floor filter
(default 0.5) so noise edges don't dilute results. seedNodes plumbed
through sublinear/page-rank-entry for query-aware personalisation.
All 5 adapters share:
- Dependency-injection via structural Source interfaces (no hard import
of @claude-flow/browser, ruflo-federation, etc. from this plugin)
- Optional nodeFilter for matrix pruning
- contentHash for memoization + Phase 7 signed-artifact integrity
- DD-safety-margin diagonal to keep the matrix solvable
63/63 tests passing (+18 for Phases 2-4):
- browser-causal-adapter.test.ts: 8 tests
- phase3-adapters.test.ts: 11 tests (federation: 4, cost: 3, observability: 3)
- phase4-adapters.test.ts: 8 tests (KG: 5, RAG: 3)
End-to-end via sublinear/page-rank-entry: 4 routed-through tests proving
the full registry → adapter → solver → result path on real graph shapes.
Co-Authored-By: RuFlo <ruv@ruv.net>
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
/**
|
|
* Phase 4 Adapter Tests — knowledge graph + RAG memory
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
KnowledgeGraphAdapter,
|
|
KNOWLEDGE_GRAPH_ID,
|
|
registerKnowledgeGraphAdapter,
|
|
} from '../src/adapters/knowledge-graph-adapter.js';
|
|
import {
|
|
RagMemoryAdapter,
|
|
ragMemoryGraphId,
|
|
registerRagMemoryAdapter,
|
|
} from '../src/adapters/rag-memory-adapter.js';
|
|
import { resetRegistry, getRegistry } from '../src/domain/adapter.js';
|
|
import { coherenceScore } from '../src/infrastructure/solver-bridge.js';
|
|
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
|
|
describe('KnowledgeGraphAdapter', () => {
|
|
beforeEach(() => resetRegistry());
|
|
|
|
it('exports a DD matrix from KG edges', async () => {
|
|
const adapter = new KnowledgeGraphAdapter({
|
|
source: {
|
|
async listEdges() {
|
|
return [
|
|
{ fromEntity: 'Claude', toEntity: 'Anthropic', relation: 'createdBy', confidence: 0.95 },
|
|
{ fromEntity: 'Anthropic', toEntity: 'AI Safety', relation: 'focuses-on', confidence: 0.9 },
|
|
{ fromEntity: 'Claude', toEntity: 'AI Safety', relation: 'aligned-with', confidence: 0.8 },
|
|
];
|
|
},
|
|
},
|
|
});
|
|
const m = await adapter.exportAsSparseMatrix();
|
|
expect(m.size).toBe(3);
|
|
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('combines multiple relations between same entities', async () => {
|
|
const adapter = new KnowledgeGraphAdapter({
|
|
source: {
|
|
async listEdges() {
|
|
return [
|
|
{ fromEntity: 'A', toEntity: 'B', relation: 'r1', confidence: 0.4 },
|
|
{ fromEntity: 'A', toEntity: 'B', relation: 'r2', confidence: 0.5 },
|
|
];
|
|
},
|
|
},
|
|
});
|
|
const m = await adapter.exportAsSparseMatrix();
|
|
const offDiag = m.entries.find((e) => e.row !== e.col);
|
|
expect(offDiag?.value).toBeCloseTo(0.9, 5);
|
|
});
|
|
|
|
it('caps combined confidence at 1.0', async () => {
|
|
const adapter = new KnowledgeGraphAdapter({
|
|
source: {
|
|
async listEdges() {
|
|
return [
|
|
{ fromEntity: 'A', toEntity: 'B', relation: 'r1', confidence: 0.7 },
|
|
{ fromEntity: 'A', toEntity: 'B', relation: 'r2', confidence: 0.8 },
|
|
];
|
|
},
|
|
},
|
|
});
|
|
const m = await adapter.exportAsSparseMatrix();
|
|
const offDiag = m.entries.find((e) => e.row !== e.col);
|
|
expect(offDiag?.value).toBe(1.0);
|
|
});
|
|
|
|
it('registers under canonical graphId', () => {
|
|
const registry = getRegistry();
|
|
registerKnowledgeGraphAdapter({
|
|
source: { async listEdges() { return []; } },
|
|
registry,
|
|
});
|
|
expect(registry.get(KNOWLEDGE_GRAPH_ID)).toBeDefined();
|
|
});
|
|
|
|
it('end-to-end via sublinear/page-rank-entry', async () => {
|
|
const registry = getRegistry();
|
|
registerKnowledgeGraphAdapter({
|
|
source: {
|
|
async listEdges() {
|
|
return [
|
|
{ fromEntity: 'A', toEntity: 'B', relation: 'r', confidence: 0.5 },
|
|
{ fromEntity: 'B', toEntity: 'C', relation: 'r', confidence: 0.5 },
|
|
{ fromEntity: 'C', toEntity: 'A', relation: 'r', confidence: 0.5 },
|
|
];
|
|
},
|
|
},
|
|
registry,
|
|
});
|
|
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
const r = (await tool!.handler({
|
|
graphId: KNOWLEDGE_GRAPH_ID,
|
|
nodeId: 'B',
|
|
maxComplexityClass: 'polynomial',
|
|
})) as { success: boolean; result?: { score: number } };
|
|
expect(r.success).toBe(true);
|
|
expect(r.result?.score).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('RagMemoryAdapter', () => {
|
|
beforeEach(() => resetRegistry());
|
|
|
|
it('exports a DD matrix from chunk edges, filtering below similarity floor', async () => {
|
|
const adapter = new RagMemoryAdapter({
|
|
similarityFloor: 0.5,
|
|
source: {
|
|
async listChunkEdges() {
|
|
return [
|
|
{ fromChunkId: 'c1', toChunkId: 'c2', similarity: 0.8 },
|
|
{ fromChunkId: 'c1', toChunkId: 'c3', similarity: 0.3 }, // filtered
|
|
{ fromChunkId: 'c2', toChunkId: 'c3', similarity: 0.7 },
|
|
];
|
|
},
|
|
},
|
|
});
|
|
const m = await adapter.exportAsSparseMatrix();
|
|
// c3 still appears via c2 → c3 (above floor); c1 → c3 is the filtered one.
|
|
expect(m.size).toBe(3);
|
|
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('namespace scopes the graph id', () => {
|
|
expect(ragMemoryGraphId('docs')).toBe('ruflo-rag-memory:chunks:docs');
|
|
expect(ragMemoryGraphId()).toBe('ruflo-rag-memory:chunks:default');
|
|
});
|
|
|
|
it('end-to-end personalized PR via seedNodes', async () => {
|
|
const registry = getRegistry();
|
|
registerRagMemoryAdapter({
|
|
namespace: 'test',
|
|
source: {
|
|
async listChunkEdges() {
|
|
return [
|
|
{ fromChunkId: 'c1', toChunkId: 'c2', similarity: 0.9 },
|
|
{ fromChunkId: 'c2', toChunkId: 'c3', similarity: 0.7 },
|
|
{ fromChunkId: 'c3', toChunkId: 'c1', similarity: 0.6 },
|
|
];
|
|
},
|
|
},
|
|
registry,
|
|
});
|
|
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
const r = (await tool!.handler({
|
|
graphId: ragMemoryGraphId('test'),
|
|
nodeId: 'c3',
|
|
seedNodes: ['c1'],
|
|
maxComplexityClass: 'polynomial',
|
|
})) as { success: boolean; result?: { score: number } };
|
|
expect(r.success).toBe(true);
|
|
expect(r.result?.score).toBeGreaterThan(0);
|
|
});
|
|
});
|