ruflo-graph-intelligence 0.1.0-alpha.1
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/.claude-flow/data/pending-insights.jsonl +30 -0
- package/dist/adapters/aidefence-suspicion-adapter.d.ts +40 -0
- package/dist/adapters/aidefence-suspicion-adapter.d.ts.map +1 -0
- package/dist/adapters/aidefence-suspicion-adapter.js +77 -0
- package/dist/adapters/aidefence-suspicion-adapter.js.map +1 -0
- package/dist/adapters/browser-causal-adapter.d.ts +83 -0
- package/dist/adapters/browser-causal-adapter.d.ts.map +1 -0
- package/dist/adapters/browser-causal-adapter.js +146 -0
- package/dist/adapters/browser-causal-adapter.js.map +1 -0
- package/dist/adapters/cost-attribution-adapter.d.ts +48 -0
- package/dist/adapters/cost-attribution-adapter.d.ts.map +1 -0
- package/dist/adapters/cost-attribution-adapter.js +95 -0
- package/dist/adapters/cost-attribution-adapter.js.map +1 -0
- package/dist/adapters/federation-trust-adapter.d.ts +49 -0
- package/dist/adapters/federation-trust-adapter.d.ts.map +1 -0
- package/dist/adapters/federation-trust-adapter.js +82 -0
- package/dist/adapters/federation-trust-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/jujutsu-blast-radius-adapter.d.ts +46 -0
- package/dist/adapters/jujutsu-blast-radius-adapter.d.ts.map +1 -0
- package/dist/adapters/jujutsu-blast-radius-adapter.js +80 -0
- package/dist/adapters/jujutsu-blast-radius-adapter.js.map +1 -0
- package/dist/adapters/knowledge-graph-adapter.d.ts +41 -0
- package/dist/adapters/knowledge-graph-adapter.d.ts.map +1 -0
- package/dist/adapters/knowledge-graph-adapter.js +83 -0
- package/dist/adapters/knowledge-graph-adapter.js.map +1 -0
- package/dist/adapters/observability-span-adapter.d.ts +45 -0
- package/dist/adapters/observability-span-adapter.d.ts.map +1 -0
- package/dist/adapters/observability-span-adapter.js +97 -0
- package/dist/adapters/observability-span-adapter.js.map +1 -0
- package/dist/adapters/portfolio-cg-adapter.d.ts +60 -0
- package/dist/adapters/portfolio-cg-adapter.d.ts.map +1 -0
- package/dist/adapters/portfolio-cg-adapter.js +102 -0
- package/dist/adapters/portfolio-cg-adapter.js.map +1 -0
- package/dist/adapters/rag-memory-adapter.d.ts +49 -0
- package/dist/adapters/rag-memory-adapter.d.ts.map +1 -0
- package/dist/adapters/rag-memory-adapter.js +86 -0
- package/dist/adapters/rag-memory-adapter.js.map +1 -0
- package/dist/application/federation-client.d.ts +54 -0
- package/dist/application/federation-client.d.ts.map +1 -0
- package/dist/application/federation-client.js +101 -0
- package/dist/application/federation-client.js.map +1 -0
- package/dist/application/federation-server.d.ts +38 -0
- package/dist/application/federation-server.d.ts.map +1 -0
- package/dist/application/federation-server.js +127 -0
- package/dist/application/federation-server.js.map +1 -0
- package/dist/application/streaming-bridge.d.ts +62 -0
- package/dist/application/streaming-bridge.d.ts.map +1 -0
- package/dist/application/streaming-bridge.js +101 -0
- package/dist/application/streaming-bridge.js.map +1 -0
- package/dist/domain/adapter.d.ts +58 -0
- package/dist/domain/adapter.d.ts.map +1 -0
- package/dist/domain/adapter.js +43 -0
- package/dist/domain/adapter.js.map +1 -0
- package/dist/domain/federation-protocol.d.ts +857 -0
- package/dist/domain/federation-protocol.d.ts.map +1 -0
- package/dist/domain/federation-protocol.js +72 -0
- package/dist/domain/federation-protocol.js.map +1 -0
- package/dist/domain/signed-artifact.d.ts +429 -0
- package/dist/domain/signed-artifact.d.ts.map +1 -0
- package/dist/domain/signed-artifact.js +57 -0
- package/dist/domain/signed-artifact.js.map +1 -0
- package/dist/domain/types.d.ts +329 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +165 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/jl-embed.d.ts +27 -0
- package/dist/infrastructure/jl-embed.d.ts.map +1 -0
- package/dist/infrastructure/jl-embed.js +79 -0
- package/dist/infrastructure/jl-embed.js.map +1 -0
- package/dist/infrastructure/solver-bridge.d.ts +73 -0
- package/dist/infrastructure/solver-bridge.d.ts.map +1 -0
- package/dist/infrastructure/solver-bridge.js +359 -0
- package/dist/infrastructure/solver-bridge.js.map +1 -0
- package/dist/infrastructure/witness-signer.d.ts +44 -0
- package/dist/infrastructure/witness-signer.d.ts.map +1 -0
- package/dist/infrastructure/witness-signer.js +158 -0
- package/dist/infrastructure/witness-signer.js.map +1 -0
- package/dist/mcp-tools/index.d.ts +27 -0
- package/dist/mcp-tools/index.d.ts.map +1 -0
- package/dist/mcp-tools/index.js +292 -0
- package/dist/mcp-tools/index.js.map +1 -0
- package/package.json +55 -0
- package/ruvector.db +0 -0
- package/src/adapters/aidefence-suspicion-adapter.ts +102 -0
- package/src/adapters/browser-causal-adapter.ts +193 -0
- package/src/adapters/cost-attribution-adapter.ts +123 -0
- package/src/adapters/federation-trust-adapter.ts +116 -0
- package/src/adapters/index.ts +87 -0
- package/src/adapters/jujutsu-blast-radius-adapter.ts +107 -0
- package/src/adapters/knowledge-graph-adapter.ts +110 -0
- package/src/adapters/observability-span-adapter.ts +123 -0
- package/src/adapters/portfolio-cg-adapter.ts +140 -0
- package/src/adapters/rag-memory-adapter.ts +117 -0
- package/src/application/federation-client.ts +147 -0
- package/src/application/federation-server.ts +158 -0
- package/src/application/streaming-bridge.ts +137 -0
- package/src/domain/adapter.ts +92 -0
- package/src/domain/federation-protocol.ts +95 -0
- package/src/domain/signed-artifact.ts +80 -0
- package/src/domain/types.ts +215 -0
- package/src/index.ts +105 -0
- package/src/infrastructure/jl-embed.ts +98 -0
- package/src/infrastructure/solver-bridge.ts +389 -0
- package/src/infrastructure/witness-signer.ts +209 -0
- package/src/mcp-tools/index.ts +316 -0
- package/tests/adapter-registry.test.ts +69 -0
- package/tests/browser-causal-adapter.test.ts +174 -0
- package/tests/mcp-tools.test.ts +169 -0
- package/tests/phase3-adapters.test.ts +206 -0
- package/tests/phase4-adapters.test.ts +158 -0
- package/tests/phase5-portfolio.test.ts +122 -0
- package/tests/phase6-adapters.test.ts +224 -0
- package/tests/phase6_5-streaming.test.ts +135 -0
- package/tests/phase7-signed-artifact.test.ts +238 -0
- package/tests/phase8-federation.test.ts +194 -0
- package/tests/solver-bridge.test.ts +255 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruflo-graph-intelligence — MCP Tool Tests (ADR-123 Phase 1)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
7
|
+
import { getRegistry, resetRegistry } from '../src/domain/adapter.js';
|
|
8
|
+
import type { SublinearAdapter } from '../src/domain/adapter.js';
|
|
9
|
+
import type { SparseMatrix } from '../src/domain/types.js';
|
|
10
|
+
|
|
11
|
+
function ddTestAdapter(): SublinearAdapter {
|
|
12
|
+
return {
|
|
13
|
+
graphId: 'test:dd',
|
|
14
|
+
ownerPlugin: 'test',
|
|
15
|
+
async exportAsSparseMatrix(): Promise<SparseMatrix> {
|
|
16
|
+
const entries = [];
|
|
17
|
+
const nodeIndex: Record<string, number> = {};
|
|
18
|
+
const indexNode: string[] = [];
|
|
19
|
+
const n = 6;
|
|
20
|
+
for (let i = 0; i < n; i++) {
|
|
21
|
+
nodeIndex[`n${i}`] = i;
|
|
22
|
+
indexNode.push(`n${i}`);
|
|
23
|
+
entries.push({ row: i, col: i, value: 5 });
|
|
24
|
+
if (i > 0) entries.push({ row: i, col: i - 1, value: -1 });
|
|
25
|
+
if (i < n - 1) entries.push({ row: i, col: i + 1, value: -1 });
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
graphId: 'test:dd',
|
|
29
|
+
size: n,
|
|
30
|
+
entries,
|
|
31
|
+
nodeIndex,
|
|
32
|
+
indexNode,
|
|
33
|
+
capturedAt: 't',
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findTool(name: string) {
|
|
40
|
+
const tool = graphIntelligenceTools.find((t) => t.name === name);
|
|
41
|
+
if (!tool) throw new Error(`tool ${name} not found`);
|
|
42
|
+
return tool;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('MCP tools — surface', () => {
|
|
46
|
+
it('exports six tools under sublinear/*', () => {
|
|
47
|
+
const names = graphIntelligenceTools.map((t) => t.name);
|
|
48
|
+
expect(names).toEqual([
|
|
49
|
+
'sublinear/page-rank-entry',
|
|
50
|
+
'sublinear/solve',
|
|
51
|
+
'sublinear/solve-on-change',
|
|
52
|
+
'sublinear/analyze',
|
|
53
|
+
'sublinear/feasibility',
|
|
54
|
+
'sublinear/jl-embed',
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('each tool has an input schema with type:object', () => {
|
|
59
|
+
for (const t of graphIntelligenceTools) {
|
|
60
|
+
expect(t.inputSchema.type).toBe('object');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('sublinear/page-rank-entry', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetRegistry();
|
|
68
|
+
getRegistry().register(ddTestAdapter());
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns a result on the happy path', async () => {
|
|
72
|
+
const tool = findTool('sublinear/page-rank-entry');
|
|
73
|
+
// Phase 1 forward-push is a reference impl — observed iterations on small DD
|
|
74
|
+
// matrices may exceed the `linear` default budget. Use `polynomial` here to
|
|
75
|
+
// exercise the happy path; the budget-exceeded path has its own dedicated test.
|
|
76
|
+
const r = (await tool.handler({
|
|
77
|
+
graphId: 'test:dd',
|
|
78
|
+
nodeId: 'n3',
|
|
79
|
+
maxComplexityClass: 'polynomial',
|
|
80
|
+
})) as { success: boolean; result?: unknown };
|
|
81
|
+
expect(r.success).toBe(true);
|
|
82
|
+
expect(r.result).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns complexity-budget-exceeded when budget is too tight', async () => {
|
|
86
|
+
const tool = findTool('sublinear/page-rank-entry');
|
|
87
|
+
const r = (await tool.handler({
|
|
88
|
+
graphId: 'test:dd',
|
|
89
|
+
nodeId: 'n3',
|
|
90
|
+
maxComplexityClass: 'constant',
|
|
91
|
+
})) as { success: boolean; error?: { kind: string } };
|
|
92
|
+
expect(r.success).toBe(false);
|
|
93
|
+
expect(r.error?.kind).toBe('complexity-budget-exceeded');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns graph-not-found for an unknown graphId', async () => {
|
|
97
|
+
const tool = findTool('sublinear/page-rank-entry');
|
|
98
|
+
const r = (await tool.handler({ graphId: 'missing', nodeId: 'n0' })) as { success: boolean; error?: { kind: string } };
|
|
99
|
+
expect(r.success).toBe(false);
|
|
100
|
+
expect(r.error?.kind).toBe('graph-not-found');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns coherence-rejected when threshold not met', async () => {
|
|
104
|
+
const tool = findTool('sublinear/page-rank-entry');
|
|
105
|
+
const r = (await tool.handler({
|
|
106
|
+
graphId: 'test:dd',
|
|
107
|
+
nodeId: 'n0',
|
|
108
|
+
coherenceThreshold: 0.99,
|
|
109
|
+
})) as { success: boolean; error?: { kind: string } };
|
|
110
|
+
expect(r.success).toBe(false);
|
|
111
|
+
expect(r.error?.kind).toBe('coherence-rejected');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('sublinear/analyze', () => {
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
resetRegistry();
|
|
118
|
+
getRegistry().register(ddTestAdapter());
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('reports coherence + recommended algorithm', async () => {
|
|
122
|
+
const tool = findTool('sublinear/analyze');
|
|
123
|
+
const r = (await tool.handler({ graphId: 'test:dd' })) as { success: boolean; result?: Record<string, unknown> };
|
|
124
|
+
expect(r.success).toBe(true);
|
|
125
|
+
expect(r.result?.size).toBe(6);
|
|
126
|
+
expect(r.result?.coherenceScore).toBeGreaterThan(0);
|
|
127
|
+
expect(r.result?.recommendedAlgorithm).toBeDefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('sublinear/solve', () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
resetRegistry();
|
|
134
|
+
getRegistry().register(ddTestAdapter());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('solves A·x = b with CG', async () => {
|
|
138
|
+
const tool = findTool('sublinear/solve');
|
|
139
|
+
const r = (await tool.handler({
|
|
140
|
+
graphId: 'test:dd',
|
|
141
|
+
rhs: [1, 1, 1, 1, 1, 1],
|
|
142
|
+
algorithm: 'cg',
|
|
143
|
+
maxComplexityClass: 'polynomial',
|
|
144
|
+
})) as { success: boolean; result?: { x: number[]; residualNorm: number } };
|
|
145
|
+
expect(r.success).toBe(true);
|
|
146
|
+
expect(r.result?.x).toHaveLength(6);
|
|
147
|
+
expect(r.result?.residualNorm).toBeLessThan(1e-3);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('sublinear/solve-on-change', () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
resetRegistry();
|
|
154
|
+
getRegistry().register(ddTestAdapter());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('handles a single-node delta', async () => {
|
|
158
|
+
const tool = findTool('sublinear/solve-on-change');
|
|
159
|
+
const r = (await tool.handler({
|
|
160
|
+
graphId: 'test:dd',
|
|
161
|
+
prevSolution: [0, 0, 0, 0, 0, 0],
|
|
162
|
+
delta: { indices: [2], values: [0.5] },
|
|
163
|
+
algorithm: 'cg',
|
|
164
|
+
maxComplexityClass: 'polynomial',
|
|
165
|
+
})) as { success: boolean; result?: { x: number[] } };
|
|
166
|
+
expect(r.success).toBe(true);
|
|
167
|
+
expect(r.result?.x).toHaveLength(6);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 Adapter Tests — federation trust + cost attribution + observability
|
|
3
|
+
*
|
|
4
|
+
* Acceptance:
|
|
5
|
+
* - Each adapter exports a DD SparseMatrix
|
|
6
|
+
* - Each adapter registers under its canonical graphId
|
|
7
|
+
* - sublinear/page-rank-entry routes through each end-to-end
|
|
8
|
+
* - Stale federation edges are pruned
|
|
9
|
+
* - Cost rows are L1-normalised so PR weights are proportional shares
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
FederationTrustAdapter,
|
|
15
|
+
FEDERATION_TRUST_GRAPH_ID,
|
|
16
|
+
registerFederationTrustAdapter,
|
|
17
|
+
} from '../src/adapters/federation-trust-adapter.js';
|
|
18
|
+
import {
|
|
19
|
+
CostAttributionAdapter,
|
|
20
|
+
costAttributionGraphId,
|
|
21
|
+
registerCostAttributionAdapter,
|
|
22
|
+
} from '../src/adapters/cost-attribution-adapter.js';
|
|
23
|
+
import {
|
|
24
|
+
ObservabilitySpanAdapter,
|
|
25
|
+
observabilityGraphId,
|
|
26
|
+
registerObservabilitySpanAdapter,
|
|
27
|
+
} from '../src/adapters/observability-span-adapter.js';
|
|
28
|
+
import { resetRegistry, getRegistry } from '../src/domain/adapter.js';
|
|
29
|
+
import { coherenceScore } from '../src/infrastructure/solver-bridge.js';
|
|
30
|
+
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
31
|
+
|
|
32
|
+
describe('FederationTrustAdapter', () => {
|
|
33
|
+
beforeEach(() => resetRegistry());
|
|
34
|
+
|
|
35
|
+
it('exports a DD matrix from peer trust edges', async () => {
|
|
36
|
+
const adapter = new FederationTrustAdapter({
|
|
37
|
+
source: {
|
|
38
|
+
async listTrustEdges() {
|
|
39
|
+
return [
|
|
40
|
+
{ fromPeer: 'A', toPeer: 'B', confidence: 0.8, updatedAt: new Date().toISOString() },
|
|
41
|
+
{ fromPeer: 'B', toPeer: 'C', confidence: 0.6, updatedAt: new Date().toISOString() },
|
|
42
|
+
{ fromPeer: 'A', toPeer: 'C', confidence: 0.3, updatedAt: new Date().toISOString() },
|
|
43
|
+
];
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
48
|
+
expect(m.size).toBe(3);
|
|
49
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('prunes stale edges past the freshness window', async () => {
|
|
53
|
+
const old = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
54
|
+
const fresh = new Date().toISOString();
|
|
55
|
+
const adapter = new FederationTrustAdapter({
|
|
56
|
+
freshnessMs: 7 * 24 * 60 * 60 * 1000,
|
|
57
|
+
source: {
|
|
58
|
+
async listTrustEdges() {
|
|
59
|
+
return [
|
|
60
|
+
{ fromPeer: 'A', toPeer: 'B', confidence: 0.8, updatedAt: old },
|
|
61
|
+
{ fromPeer: 'B', toPeer: 'C', confidence: 0.6, updatedAt: fresh },
|
|
62
|
+
];
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
67
|
+
expect(Object.keys(m.nodeIndex).sort()).toEqual(['B', 'C']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('registers under the canonical graphId', () => {
|
|
71
|
+
const registry = getRegistry();
|
|
72
|
+
registerFederationTrustAdapter({
|
|
73
|
+
source: { async listTrustEdges() { return []; } },
|
|
74
|
+
registry,
|
|
75
|
+
});
|
|
76
|
+
expect(registry.get(FEDERATION_TRUST_GRAPH_ID)).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('end-to-end via sublinear/page-rank-entry', async () => {
|
|
80
|
+
const registry = getRegistry();
|
|
81
|
+
registerFederationTrustAdapter({
|
|
82
|
+
source: {
|
|
83
|
+
async listTrustEdges() {
|
|
84
|
+
return [
|
|
85
|
+
{ fromPeer: 'A', toPeer: 'B', confidence: 0.9, updatedAt: new Date().toISOString() },
|
|
86
|
+
{ fromPeer: 'B', toPeer: 'C', confidence: 0.7, updatedAt: new Date().toISOString() },
|
|
87
|
+
];
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
registry,
|
|
91
|
+
});
|
|
92
|
+
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
93
|
+
const r = (await tool!.handler({
|
|
94
|
+
graphId: FEDERATION_TRUST_GRAPH_ID,
|
|
95
|
+
nodeId: 'C',
|
|
96
|
+
maxComplexityClass: 'polynomial',
|
|
97
|
+
})) as { success: boolean; result?: { score: number } };
|
|
98
|
+
expect(r.success).toBe(true);
|
|
99
|
+
expect(r.result?.score).toBeGreaterThanOrEqual(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('CostAttributionAdapter', () => {
|
|
104
|
+
beforeEach(() => resetRegistry());
|
|
105
|
+
|
|
106
|
+
it('builds a DD matrix from causation edges', async () => {
|
|
107
|
+
const adapter = new CostAttributionAdapter({
|
|
108
|
+
sessionId: 'sess-1',
|
|
109
|
+
source: {
|
|
110
|
+
async listCausationEdges() {
|
|
111
|
+
return [
|
|
112
|
+
{ parentId: 'prompt-1', childId: 'agent-1', costUsd: 0.5 },
|
|
113
|
+
{ parentId: 'prompt-1', childId: 'agent-2', costUsd: 0.2 },
|
|
114
|
+
{ parentId: 'agent-1', childId: 'model-call-1', costUsd: 0.05 },
|
|
115
|
+
];
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
120
|
+
expect(m.size).toBeGreaterThan(0);
|
|
121
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('L1-normalises costs into proportional shares per parent', async () => {
|
|
125
|
+
const adapter = new CostAttributionAdapter({
|
|
126
|
+
source: {
|
|
127
|
+
async listCausationEdges() {
|
|
128
|
+
return [
|
|
129
|
+
{ parentId: 'p', childId: 'a', costUsd: 1.0 },
|
|
130
|
+
{ parentId: 'p', childId: 'b', costUsd: 3.0 },
|
|
131
|
+
];
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
136
|
+
const pIdx = m.nodeIndex['p'];
|
|
137
|
+
const aIdx = m.nodeIndex['a'];
|
|
138
|
+
const bIdx = m.nodeIndex['b'];
|
|
139
|
+
const pa = m.entries.find((e) => e.row === pIdx && e.col === aIdx)?.value ?? 0;
|
|
140
|
+
const pb = m.entries.find((e) => e.row === pIdx && e.col === bIdx)?.value ?? 0;
|
|
141
|
+
expect(pa + pb).toBeCloseTo(1.0, 6);
|
|
142
|
+
expect(pb / pa).toBeCloseTo(3, 2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('registers under the canonical session-scoped graphId', () => {
|
|
146
|
+
const registry = getRegistry();
|
|
147
|
+
registerCostAttributionAdapter({
|
|
148
|
+
sessionId: 's',
|
|
149
|
+
source: { async listCausationEdges() { return []; } },
|
|
150
|
+
registry,
|
|
151
|
+
});
|
|
152
|
+
expect(registry.get(costAttributionGraphId('s'))).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('ObservabilitySpanAdapter', () => {
|
|
157
|
+
beforeEach(() => resetRegistry());
|
|
158
|
+
|
|
159
|
+
it('builds a DD matrix from span tree', async () => {
|
|
160
|
+
const adapter = new ObservabilitySpanAdapter({
|
|
161
|
+
traceId: 't1',
|
|
162
|
+
source: {
|
|
163
|
+
async listSpans() {
|
|
164
|
+
return [
|
|
165
|
+
{ spanId: 'root', traceId: 't1', selfTimeUs: 100 },
|
|
166
|
+
{ spanId: 'child-a', parentSpanId: 'root', traceId: 't1', selfTimeUs: 60 },
|
|
167
|
+
{ spanId: 'child-b', parentSpanId: 'root', traceId: 't1', selfTimeUs: 40 },
|
|
168
|
+
{ spanId: 'leaf', parentSpanId: 'child-a', traceId: 't1', selfTimeUs: 10 },
|
|
169
|
+
];
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
174
|
+
expect(m.size).toBe(4);
|
|
175
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('honours cross-trace causedBy links', async () => {
|
|
179
|
+
const adapter = new ObservabilitySpanAdapter({
|
|
180
|
+
traceId: 't1',
|
|
181
|
+
source: {
|
|
182
|
+
async listSpans() {
|
|
183
|
+
return [
|
|
184
|
+
{ spanId: 'a', traceId: 't1', selfTimeUs: 100 },
|
|
185
|
+
{ spanId: 'b', traceId: 't1', selfTimeUs: 50, causedBySpanId: 'a' },
|
|
186
|
+
];
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
191
|
+
const aIdx = m.nodeIndex['a'];
|
|
192
|
+
const bIdx = m.nodeIndex['b'];
|
|
193
|
+
const ab = m.entries.find((e) => e.row === aIdx && e.col === bIdx);
|
|
194
|
+
expect(ab?.value).toBeGreaterThan(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('registers under the canonical graphId', () => {
|
|
198
|
+
const registry = getRegistry();
|
|
199
|
+
registerObservabilitySpanAdapter({
|
|
200
|
+
traceId: 't',
|
|
201
|
+
source: { async listSpans() { return []; } },
|
|
202
|
+
registry,
|
|
203
|
+
});
|
|
204
|
+
expect(registry.get(observabilityGraphId('t'))).toBeDefined();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 Adapter Tests — knowledge graph + RAG memory
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
KnowledgeGraphAdapter,
|
|
8
|
+
KNOWLEDGE_GRAPH_ID,
|
|
9
|
+
registerKnowledgeGraphAdapter,
|
|
10
|
+
} from '../src/adapters/knowledge-graph-adapter.js';
|
|
11
|
+
import {
|
|
12
|
+
RagMemoryAdapter,
|
|
13
|
+
ragMemoryGraphId,
|
|
14
|
+
registerRagMemoryAdapter,
|
|
15
|
+
} from '../src/adapters/rag-memory-adapter.js';
|
|
16
|
+
import { resetRegistry, getRegistry } from '../src/domain/adapter.js';
|
|
17
|
+
import { coherenceScore } from '../src/infrastructure/solver-bridge.js';
|
|
18
|
+
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
19
|
+
|
|
20
|
+
describe('KnowledgeGraphAdapter', () => {
|
|
21
|
+
beforeEach(() => resetRegistry());
|
|
22
|
+
|
|
23
|
+
it('exports a DD matrix from KG edges', async () => {
|
|
24
|
+
const adapter = new KnowledgeGraphAdapter({
|
|
25
|
+
source: {
|
|
26
|
+
async listEdges() {
|
|
27
|
+
return [
|
|
28
|
+
{ fromEntity: 'Claude', toEntity: 'Anthropic', relation: 'createdBy', confidence: 0.95 },
|
|
29
|
+
{ fromEntity: 'Anthropic', toEntity: 'AI Safety', relation: 'focuses-on', confidence: 0.9 },
|
|
30
|
+
{ fromEntity: 'Claude', toEntity: 'AI Safety', relation: 'aligned-with', confidence: 0.8 },
|
|
31
|
+
];
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
36
|
+
expect(m.size).toBe(3);
|
|
37
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('combines multiple relations between same entities', async () => {
|
|
41
|
+
const adapter = new KnowledgeGraphAdapter({
|
|
42
|
+
source: {
|
|
43
|
+
async listEdges() {
|
|
44
|
+
return [
|
|
45
|
+
{ fromEntity: 'A', toEntity: 'B', relation: 'r1', confidence: 0.4 },
|
|
46
|
+
{ fromEntity: 'A', toEntity: 'B', relation: 'r2', confidence: 0.5 },
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
52
|
+
const offDiag = m.entries.find((e) => e.row !== e.col);
|
|
53
|
+
expect(offDiag?.value).toBeCloseTo(0.9, 5);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('caps combined confidence at 1.0', async () => {
|
|
57
|
+
const adapter = new KnowledgeGraphAdapter({
|
|
58
|
+
source: {
|
|
59
|
+
async listEdges() {
|
|
60
|
+
return [
|
|
61
|
+
{ fromEntity: 'A', toEntity: 'B', relation: 'r1', confidence: 0.7 },
|
|
62
|
+
{ fromEntity: 'A', toEntity: 'B', relation: 'r2', confidence: 0.8 },
|
|
63
|
+
];
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
68
|
+
const offDiag = m.entries.find((e) => e.row !== e.col);
|
|
69
|
+
expect(offDiag?.value).toBe(1.0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('registers under canonical graphId', () => {
|
|
73
|
+
const registry = getRegistry();
|
|
74
|
+
registerKnowledgeGraphAdapter({
|
|
75
|
+
source: { async listEdges() { return []; } },
|
|
76
|
+
registry,
|
|
77
|
+
});
|
|
78
|
+
expect(registry.get(KNOWLEDGE_GRAPH_ID)).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('end-to-end via sublinear/page-rank-entry', async () => {
|
|
82
|
+
const registry = getRegistry();
|
|
83
|
+
registerKnowledgeGraphAdapter({
|
|
84
|
+
source: {
|
|
85
|
+
async listEdges() {
|
|
86
|
+
return [
|
|
87
|
+
{ fromEntity: 'A', toEntity: 'B', relation: 'r', confidence: 0.5 },
|
|
88
|
+
{ fromEntity: 'B', toEntity: 'C', relation: 'r', confidence: 0.5 },
|
|
89
|
+
{ fromEntity: 'C', toEntity: 'A', relation: 'r', confidence: 0.5 },
|
|
90
|
+
];
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
registry,
|
|
94
|
+
});
|
|
95
|
+
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
96
|
+
const r = (await tool!.handler({
|
|
97
|
+
graphId: KNOWLEDGE_GRAPH_ID,
|
|
98
|
+
nodeId: 'B',
|
|
99
|
+
maxComplexityClass: 'polynomial',
|
|
100
|
+
})) as { success: boolean; result?: { score: number } };
|
|
101
|
+
expect(r.success).toBe(true);
|
|
102
|
+
expect(r.result?.score).toBeGreaterThan(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('RagMemoryAdapter', () => {
|
|
107
|
+
beforeEach(() => resetRegistry());
|
|
108
|
+
|
|
109
|
+
it('exports a DD matrix from chunk edges, filtering below similarity floor', async () => {
|
|
110
|
+
const adapter = new RagMemoryAdapter({
|
|
111
|
+
similarityFloor: 0.5,
|
|
112
|
+
source: {
|
|
113
|
+
async listChunkEdges() {
|
|
114
|
+
return [
|
|
115
|
+
{ fromChunkId: 'c1', toChunkId: 'c2', similarity: 0.8 },
|
|
116
|
+
{ fromChunkId: 'c1', toChunkId: 'c3', similarity: 0.3 }, // filtered
|
|
117
|
+
{ fromChunkId: 'c2', toChunkId: 'c3', similarity: 0.7 },
|
|
118
|
+
];
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
123
|
+
// c3 still appears via c2 → c3 (above floor); c1 → c3 is the filtered one.
|
|
124
|
+
expect(m.size).toBe(3);
|
|
125
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('namespace scopes the graph id', () => {
|
|
129
|
+
expect(ragMemoryGraphId('docs')).toBe('ruflo-rag-memory:chunks:docs');
|
|
130
|
+
expect(ragMemoryGraphId()).toBe('ruflo-rag-memory:chunks:default');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('end-to-end personalized PR via seedNodes', async () => {
|
|
134
|
+
const registry = getRegistry();
|
|
135
|
+
registerRagMemoryAdapter({
|
|
136
|
+
namespace: 'test',
|
|
137
|
+
source: {
|
|
138
|
+
async listChunkEdges() {
|
|
139
|
+
return [
|
|
140
|
+
{ fromChunkId: 'c1', toChunkId: 'c2', similarity: 0.9 },
|
|
141
|
+
{ fromChunkId: 'c2', toChunkId: 'c3', similarity: 0.7 },
|
|
142
|
+
{ fromChunkId: 'c3', toChunkId: 'c1', similarity: 0.6 },
|
|
143
|
+
];
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
registry,
|
|
147
|
+
});
|
|
148
|
+
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
149
|
+
const r = (await tool!.handler({
|
|
150
|
+
graphId: ragMemoryGraphId('test'),
|
|
151
|
+
nodeId: 'c3',
|
|
152
|
+
seedNodes: ['c1'],
|
|
153
|
+
maxComplexityClass: 'polynomial',
|
|
154
|
+
})) as { success: boolean; result?: { score: number } };
|
|
155
|
+
expect(r.success).toBe(true);
|
|
156
|
+
expect(r.result?.score).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 Tests — Portfolio Covariance Adapter (Wedge 8)
|
|
3
|
+
*
|
|
4
|
+
* Acceptance:
|
|
5
|
+
* - Covariance matrix is symmetric after symmetrisation
|
|
6
|
+
* - Σx = μ solved via CG to small residual
|
|
7
|
+
* - Ridge keeps Σ SPD even when the empirical covariance is rank-1
|
|
8
|
+
* - End-to-end via sublinear/solve
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
PortfolioCovarianceAdapter,
|
|
14
|
+
portfolioGraphId,
|
|
15
|
+
registerPortfolioCovarianceAdapter,
|
|
16
|
+
} from '../src/adapters/portfolio-cg-adapter.js';
|
|
17
|
+
import { resetRegistry, getRegistry } from '../src/domain/adapter.js';
|
|
18
|
+
import { conjugateGradient } from '../src/infrastructure/solver-bridge.js';
|
|
19
|
+
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
20
|
+
|
|
21
|
+
describe('PortfolioCovarianceAdapter', () => {
|
|
22
|
+
beforeEach(() => resetRegistry());
|
|
23
|
+
|
|
24
|
+
it('symmetrises one-sided covariance entries', async () => {
|
|
25
|
+
const adapter = new PortfolioCovarianceAdapter({
|
|
26
|
+
portfolioId: 'p1',
|
|
27
|
+
source: {
|
|
28
|
+
async listCovarianceEntries() {
|
|
29
|
+
return [
|
|
30
|
+
{ assetA: 'AAPL', assetB: 'AAPL', covariance: 0.04 },
|
|
31
|
+
{ assetA: 'GOOG', assetB: 'GOOG', covariance: 0.05 },
|
|
32
|
+
{ assetA: 'AAPL', assetB: 'GOOG', covariance: 0.01 }, // only one side
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
async listExpectedReturns() { return { AAPL: 0.08, GOOG: 0.09 }; },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
39
|
+
const aIdx = m.nodeIndex['AAPL'];
|
|
40
|
+
const gIdx = m.nodeIndex['GOOG'];
|
|
41
|
+
const ag = m.entries.find((e) => e.row === aIdx && e.col === gIdx);
|
|
42
|
+
const ga = m.entries.find((e) => e.row === gIdx && e.col === aIdx);
|
|
43
|
+
expect(ag?.value).toBeCloseTo(0.01, 6);
|
|
44
|
+
expect(ga?.value).toBeCloseTo(0.01, 6);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('CG solves Σx = μ to small residual', async () => {
|
|
48
|
+
const adapter = new PortfolioCovarianceAdapter({
|
|
49
|
+
portfolioId: 'p1',
|
|
50
|
+
ridge: 1e-3,
|
|
51
|
+
source: {
|
|
52
|
+
async listCovarianceEntries() {
|
|
53
|
+
return [
|
|
54
|
+
{ assetA: 'AAPL', assetB: 'AAPL', covariance: 0.04 },
|
|
55
|
+
{ assetA: 'GOOG', assetB: 'GOOG', covariance: 0.05 },
|
|
56
|
+
{ assetA: 'MSFT', assetB: 'MSFT', covariance: 0.03 },
|
|
57
|
+
{ assetA: 'AAPL', assetB: 'GOOG', covariance: 0.015 },
|
|
58
|
+
{ assetA: 'AAPL', assetB: 'MSFT', covariance: 0.018 },
|
|
59
|
+
{ assetA: 'GOOG', assetB: 'MSFT', covariance: 0.020 },
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
async listExpectedReturns() { return { AAPL: 0.08, GOOG: 0.09, MSFT: 0.07 }; },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
66
|
+
const mu = await adapter.expectedReturnsVector(m);
|
|
67
|
+
const { x, residualNorm, iterations } = conjugateGradient(m, mu, { epsilon: 1e-8, maxIter: 50 });
|
|
68
|
+
expect(x).toHaveLength(3);
|
|
69
|
+
expect(residualNorm).toBeLessThan(1e-6);
|
|
70
|
+
expect(iterations).toBeLessThan(20);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('ridge keeps a rank-deficient matrix solvable', async () => {
|
|
74
|
+
// Two perfectly-correlated assets — empirical Σ is rank 1
|
|
75
|
+
const adapter = new PortfolioCovarianceAdapter({
|
|
76
|
+
portfolioId: 'p-corr',
|
|
77
|
+
ridge: 1e-3,
|
|
78
|
+
source: {
|
|
79
|
+
async listCovarianceEntries() {
|
|
80
|
+
return [
|
|
81
|
+
{ assetA: 'X', assetB: 'X', covariance: 0.01 },
|
|
82
|
+
{ assetA: 'Y', assetB: 'Y', covariance: 0.01 },
|
|
83
|
+
{ assetA: 'X', assetB: 'Y', covariance: 0.01 },
|
|
84
|
+
];
|
|
85
|
+
},
|
|
86
|
+
async listExpectedReturns() { return { X: 0.1, Y: 0.1 }; },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
90
|
+
const mu = await adapter.expectedReturnsVector(m);
|
|
91
|
+
const { residualNorm } = conjugateGradient(m, mu, { epsilon: 1e-6, maxIter: 100 });
|
|
92
|
+
expect(residualNorm).toBeLessThan(1e-4);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('end-to-end via sublinear/solve', async () => {
|
|
96
|
+
const registry = getRegistry();
|
|
97
|
+
registerPortfolioCovarianceAdapter({
|
|
98
|
+
portfolioId: 'p2',
|
|
99
|
+
source: {
|
|
100
|
+
async listCovarianceEntries() {
|
|
101
|
+
return [
|
|
102
|
+
{ assetA: 'A', assetB: 'A', covariance: 0.04 },
|
|
103
|
+
{ assetA: 'B', assetB: 'B', covariance: 0.05 },
|
|
104
|
+
{ assetA: 'A', assetB: 'B', covariance: 0.01 },
|
|
105
|
+
];
|
|
106
|
+
},
|
|
107
|
+
async listExpectedReturns() { return { A: 0.1, B: 0.12 }; },
|
|
108
|
+
},
|
|
109
|
+
registry,
|
|
110
|
+
});
|
|
111
|
+
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/solve');
|
|
112
|
+
const r = (await tool!.handler({
|
|
113
|
+
graphId: portfolioGraphId('p2'),
|
|
114
|
+
rhs: [0.1, 0.12],
|
|
115
|
+
algorithm: 'cg',
|
|
116
|
+
maxComplexityClass: 'polynomial',
|
|
117
|
+
})) as { success: boolean; result?: { x: number[]; residualNorm: number } };
|
|
118
|
+
expect(r.success).toBe(true);
|
|
119
|
+
expect(r.result?.x).toHaveLength(2);
|
|
120
|
+
expect(r.result?.residualNorm).toBeLessThan(1e-4);
|
|
121
|
+
});
|
|
122
|
+
});
|