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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruflo-graph-intelligence — MCP Tool Surface (ADR-123 § Architecture)
|
|
3
|
+
*
|
|
4
|
+
* Six tools mounted under `sublinear/*`:
|
|
5
|
+
* - sublinear/page-rank-entry — single-entry PPR (workhorse)
|
|
6
|
+
* - sublinear/solve — full A·x = b
|
|
7
|
+
* - sublinear/solve-on-change — incremental delta (Wedge 12, streaming)
|
|
8
|
+
* - sublinear/feasibility — packing/covering LP feasibility
|
|
9
|
+
* - sublinear/jl-embed — Johnson-Lindenstrauss projection
|
|
10
|
+
* - sublinear/analyze — diagnostics (coherence, sparsity, recommended algo)
|
|
11
|
+
*
|
|
12
|
+
* Every tool accepts maxComplexityClass + coherenceThreshold.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getRegistry } from '../domain/adapter.js';
|
|
16
|
+
import {
|
|
17
|
+
PageRankQuerySchema,
|
|
18
|
+
SolveQuerySchema,
|
|
19
|
+
SolveOnChangeQuerySchema,
|
|
20
|
+
} from '../domain/types.js';
|
|
21
|
+
import {
|
|
22
|
+
runPageRank,
|
|
23
|
+
runSolve,
|
|
24
|
+
runSolveOnChange,
|
|
25
|
+
coherenceScore,
|
|
26
|
+
checkCoherence,
|
|
27
|
+
} from '../infrastructure/solver-bridge.js';
|
|
28
|
+
|
|
29
|
+
export interface MCPTool {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
category: string;
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object';
|
|
35
|
+
properties: Record<string, unknown>;
|
|
36
|
+
required?: string[];
|
|
37
|
+
};
|
|
38
|
+
handler: (input: Record<string, unknown>) => Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const graphIntelligenceTools: MCPTool[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'sublinear/page-rank-entry',
|
|
44
|
+
description:
|
|
45
|
+
'Single-entry personalized PageRank over a registered RuFlo graph. O(log n) on DD inputs. Returns score + observed complexity-class + coherence margin. Accepts maxComplexityClass budget gate (default linear) and coherenceThreshold (default 0 = disabled).',
|
|
46
|
+
category: 'graph-intelligence',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
graphId: { type: 'string', description: 'Adapter-registered graph identifier' },
|
|
51
|
+
nodeId: { type: 'string', description: 'Node to compute PR score for (single-entry query)' },
|
|
52
|
+
alpha: { type: 'number', description: 'Damping factor (default 0.85)' },
|
|
53
|
+
epsilon: { type: 'number', description: 'Convergence target (default 1e-3)' },
|
|
54
|
+
seedNodes: {
|
|
55
|
+
type: 'array',
|
|
56
|
+
items: { type: 'string' },
|
|
57
|
+
description: 'For personalized PR — seed nodes carrying restart distribution',
|
|
58
|
+
},
|
|
59
|
+
maxComplexityClass: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: '12-tier upstream class budget (constant/logarithmic/polylogarithmic/sublinear/linear/...); default linear',
|
|
62
|
+
},
|
|
63
|
+
coherenceThreshold: {
|
|
64
|
+
type: 'number',
|
|
65
|
+
description: 'DD margin floor in [-∞, 1] (default 0 = disabled)',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ['graphId', 'nodeId'],
|
|
69
|
+
},
|
|
70
|
+
handler: async (input) => {
|
|
71
|
+
const query = PageRankQuerySchema.parse(input);
|
|
72
|
+
const adapter = getRegistry().get(query.graphId);
|
|
73
|
+
if (!adapter) {
|
|
74
|
+
return { success: false, error: { kind: 'graph-not-found', message: `no adapter for graphId=${query.graphId}` } };
|
|
75
|
+
}
|
|
76
|
+
const matrix = await adapter.exportAsSparseMatrix();
|
|
77
|
+
try {
|
|
78
|
+
const result = runPageRank(matrix, query);
|
|
79
|
+
return { success: true, result };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { success: false, error: err };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
name: 'sublinear/solve',
|
|
88
|
+
description:
|
|
89
|
+
'Full linear solve A·x = b over a registered graph. CG (symmetric PD) or Neumann (general DD). Returns x + residual + observed complexity-class + coherence margin.',
|
|
90
|
+
category: 'graph-intelligence',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
graphId: { type: 'string' },
|
|
95
|
+
rhs: { type: 'array', items: { type: 'number' } },
|
|
96
|
+
algorithm: { type: 'string', enum: ['cg', 'neumann', 'random-walk'] },
|
|
97
|
+
maxComplexityClass: { type: 'string' },
|
|
98
|
+
coherenceThreshold: { type: 'number' },
|
|
99
|
+
},
|
|
100
|
+
required: ['graphId', 'rhs'],
|
|
101
|
+
},
|
|
102
|
+
handler: async (input) => {
|
|
103
|
+
const query = SolveQuerySchema.parse(input);
|
|
104
|
+
const adapter = getRegistry().get(query.graphId);
|
|
105
|
+
if (!adapter) {
|
|
106
|
+
return { success: false, error: { kind: 'graph-not-found', message: `no adapter for graphId=${query.graphId}` } };
|
|
107
|
+
}
|
|
108
|
+
const matrix = await adapter.exportAsSparseMatrix();
|
|
109
|
+
try {
|
|
110
|
+
const result = runSolve(matrix, query);
|
|
111
|
+
return { success: true, result };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return { success: false, error: err };
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
name: 'sublinear/solve-on-change',
|
|
120
|
+
description:
|
|
121
|
+
'Incremental solve A·dx = δ then x_new = x_prev + dx (Wedge 12, ADR-123). For event-driven streaming systems (federation trust deltas, span streams, append-only causal breaks). Sparse δ → asymptotically faster than full re-solve.',
|
|
122
|
+
category: 'graph-intelligence',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
graphId: { type: 'string' },
|
|
127
|
+
prevSolution: { type: 'array', items: { type: 'number' } },
|
|
128
|
+
delta: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
indices: { type: 'array', items: { type: 'number' } },
|
|
132
|
+
values: { type: 'array', items: { type: 'number' } },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
algorithm: { type: 'string', enum: ['cg', 'neumann'] },
|
|
136
|
+
maxComplexityClass: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
required: ['graphId', 'prevSolution', 'delta'],
|
|
139
|
+
},
|
|
140
|
+
handler: async (input) => {
|
|
141
|
+
const query = SolveOnChangeQuerySchema.parse(input);
|
|
142
|
+
const adapter = getRegistry().get(query.graphId);
|
|
143
|
+
if (!adapter) {
|
|
144
|
+
return { success: false, error: { kind: 'graph-not-found', message: `no adapter for graphId=${query.graphId}` } };
|
|
145
|
+
}
|
|
146
|
+
const matrix = await adapter.exportAsSparseMatrix();
|
|
147
|
+
try {
|
|
148
|
+
const result = runSolveOnChange(matrix, query);
|
|
149
|
+
return { success: true, result };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { success: false, error: err };
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
name: 'sublinear/analyze',
|
|
158
|
+
description:
|
|
159
|
+
'Diagnostic report on a registered graph: coherence margin (DD), sparsity, square-size, recommended algorithm. Use before sublinear/solve to choose algorithm + budget.',
|
|
160
|
+
category: 'graph-intelligence',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: { graphId: { type: 'string' } },
|
|
164
|
+
required: ['graphId'],
|
|
165
|
+
},
|
|
166
|
+
handler: async (input) => {
|
|
167
|
+
const graphId = input.graphId as string;
|
|
168
|
+
const adapter = getRegistry().get(graphId);
|
|
169
|
+
if (!adapter) {
|
|
170
|
+
return { success: false, error: { kind: 'graph-not-found', message: `no adapter for graphId=${graphId}` } };
|
|
171
|
+
}
|
|
172
|
+
const matrix = await adapter.exportAsSparseMatrix();
|
|
173
|
+
const coherence = checkCoherence(matrix, 0);
|
|
174
|
+
const nonzeros = matrix.entries.length;
|
|
175
|
+
const density = nonzeros / (matrix.size * matrix.size);
|
|
176
|
+
const recommendedAlgorithm = density < 0.01 ? 'forward-push' : coherence.score > 0 ? 'cg' : 'neumann';
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
result: {
|
|
180
|
+
graphId,
|
|
181
|
+
size: matrix.size,
|
|
182
|
+
nonzeros,
|
|
183
|
+
density,
|
|
184
|
+
coherenceScore: coherence.score,
|
|
185
|
+
isDiagonallyDominant: coherence.score > 0,
|
|
186
|
+
recommendedAlgorithm,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
name: 'sublinear/feasibility',
|
|
194
|
+
description:
|
|
195
|
+
'Packing/covering LP feasibility check (Kyng-Sachdeva style). Wedge 9 — pre-flight check before invoking A* / heavy planners.',
|
|
196
|
+
category: 'graph-intelligence',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
constraints: { type: 'array', description: 'A·x ≤ b constraint set' },
|
|
201
|
+
tolerance: { type: 'number', description: 'Slack for soft constraints (default 0.05)' },
|
|
202
|
+
maxComplexityClass: { type: 'string' },
|
|
203
|
+
},
|
|
204
|
+
required: ['constraints'],
|
|
205
|
+
},
|
|
206
|
+
handler: async (input) => {
|
|
207
|
+
// Phase 6: relaxed packing/covering LP. Each constraint is a row Aᵢ
|
|
208
|
+
// with shape { coeffs: Record<varId, number>, bound: number, kind: 'leq'|'geq'|'eq' }.
|
|
209
|
+
// The relaxed check: does there exist x ≥ 0 satisfying all constraints within `tolerance`?
|
|
210
|
+
// For Phase 6 we ship a tight bounded-variable LP via a simple Lagrangian
|
|
211
|
+
// shrink-on-violation pass. Real Kyng–Sachdeva solver wires in Phase 7+.
|
|
212
|
+
const constraints = (input.constraints as Array<{
|
|
213
|
+
coeffs: Record<string, number>;
|
|
214
|
+
bound: number;
|
|
215
|
+
kind?: 'leq' | 'geq' | 'eq';
|
|
216
|
+
}>) ?? [];
|
|
217
|
+
const tolerance = (input.tolerance as number) ?? 0.05;
|
|
218
|
+
if (constraints.length === 0) {
|
|
219
|
+
return { success: true, result: { feasible: true, witness: {}, method: 'no-constraints' } };
|
|
220
|
+
}
|
|
221
|
+
// Collect variables; initialise x = 0 (the trivial point).
|
|
222
|
+
const varSet = new Set<string>();
|
|
223
|
+
for (const c of constraints) for (const k of Object.keys(c.coeffs)) varSet.add(k);
|
|
224
|
+
const vars = [...varSet];
|
|
225
|
+
const x: Record<string, number> = {};
|
|
226
|
+
for (const v of vars) x[v] = 0;
|
|
227
|
+
// 200-iter Lagrangian shrink: for each violated row, push x toward
|
|
228
|
+
// satisfaction by a small step proportional to violation magnitude.
|
|
229
|
+
const stepSize = 0.05;
|
|
230
|
+
for (let it = 0; it < 200; it++) {
|
|
231
|
+
let maxViolation = 0;
|
|
232
|
+
for (const c of constraints) {
|
|
233
|
+
let lhs = 0;
|
|
234
|
+
for (const [k, w] of Object.entries(c.coeffs)) lhs += (x[k] ?? 0) * w;
|
|
235
|
+
const kind = c.kind ?? 'leq';
|
|
236
|
+
let violation = 0;
|
|
237
|
+
if (kind === 'leq' && lhs > c.bound) violation = lhs - c.bound;
|
|
238
|
+
else if (kind === 'geq' && lhs < c.bound) violation = c.bound - lhs;
|
|
239
|
+
else if (kind === 'eq') violation = Math.abs(lhs - c.bound);
|
|
240
|
+
if (violation > maxViolation) maxViolation = violation;
|
|
241
|
+
if (violation === 0) continue;
|
|
242
|
+
for (const [k, w] of Object.entries(c.coeffs)) {
|
|
243
|
+
if (w === 0) continue;
|
|
244
|
+
const direction = kind === 'leq' ? -Math.sign(w) : Math.sign(w);
|
|
245
|
+
x[k] = Math.max(0, (x[k] ?? 0) + direction * stepSize * (violation / Math.abs(w)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (maxViolation <= tolerance) {
|
|
249
|
+
return { success: true, result: { feasible: true, witness: x, iterations: it + 1, method: 'lagrangian-shrink' } };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Couldn't satisfy within iteration cap — infeasibility certificate
|
|
253
|
+
// is the residual violation vector.
|
|
254
|
+
const residuals = constraints.map((c) => {
|
|
255
|
+
let lhs = 0;
|
|
256
|
+
for (const [k, w] of Object.entries(c.coeffs)) lhs += (x[k] ?? 0) * w;
|
|
257
|
+
return { lhs, bound: c.bound, kind: c.kind ?? 'leq' };
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
result: {
|
|
262
|
+
feasible: false,
|
|
263
|
+
witness: x,
|
|
264
|
+
certificateOfInfeasibility: residuals,
|
|
265
|
+
method: 'lagrangian-shrink (capped)',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
name: 'sublinear/jl-embed',
|
|
273
|
+
description:
|
|
274
|
+
'Johnson-Lindenstrauss projection. Maps vectors to a target dimension with ε-distortion. Replaces @claude-flow/embeddings hand-rolled JL (closes ADR-121 Phase 4 follow-up).',
|
|
275
|
+
category: 'graph-intelligence',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
vectors: { type: 'array', description: 'Input vectors' },
|
|
280
|
+
targetDim: { type: 'number' },
|
|
281
|
+
epsilon: { type: 'number' },
|
|
282
|
+
},
|
|
283
|
+
required: ['vectors', 'targetDim'],
|
|
284
|
+
},
|
|
285
|
+
handler: async (input) => {
|
|
286
|
+
// Phase 6: real JL via jlEmbed (replaces ADR-121 hand-rolled).
|
|
287
|
+
const { jlEmbed } = await import('../infrastructure/jl-embed.js');
|
|
288
|
+
const vectors = (input.vectors as number[][]) ?? [];
|
|
289
|
+
const targetDim = (input.targetDim as number) ?? 64;
|
|
290
|
+
const epsilon = (input.epsilon as number) ?? 0.1;
|
|
291
|
+
try {
|
|
292
|
+
const result = jlEmbed(vectors, { targetDim, epsilon });
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
result: {
|
|
296
|
+
projected: result.projected,
|
|
297
|
+
targetDim: result.targetDim,
|
|
298
|
+
distortionBound: result.epsilon,
|
|
299
|
+
withinAchlioptasBound: result.withinAchlioptasBound,
|
|
300
|
+
method: 'real JL — Gaussian projection with k ≤ n−1 cap',
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: {
|
|
307
|
+
kind: 'invalid-input',
|
|
308
|
+
message: err instanceof Error ? err.message : String(err),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
export default graphIntelligenceTools;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruflo-graph-intelligence — Adapter Registry Tests (ADR-123 Phase 1)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { AdapterRegistry, getRegistry, resetRegistry } from '../src/domain/adapter.js';
|
|
7
|
+
import type { SublinearAdapter } from '../src/domain/adapter.js';
|
|
8
|
+
import type { SparseMatrix } from '../src/domain/types.js';
|
|
9
|
+
|
|
10
|
+
function fakeAdapter(graphId: string): SublinearAdapter {
|
|
11
|
+
return {
|
|
12
|
+
graphId,
|
|
13
|
+
ownerPlugin: 'test',
|
|
14
|
+
async exportAsSparseMatrix(): Promise<SparseMatrix> {
|
|
15
|
+
return {
|
|
16
|
+
graphId,
|
|
17
|
+
size: 1,
|
|
18
|
+
entries: [{ row: 0, col: 0, value: 1 }],
|
|
19
|
+
nodeIndex: { only: 0 },
|
|
20
|
+
indexNode: ['only'],
|
|
21
|
+
capturedAt: 't',
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AdapterRegistry', () => {
|
|
28
|
+
beforeEach(() => resetRegistry());
|
|
29
|
+
|
|
30
|
+
it('registers + retrieves an adapter', () => {
|
|
31
|
+
const r = new AdapterRegistry();
|
|
32
|
+
const a = fakeAdapter('test:graph');
|
|
33
|
+
r.register(a);
|
|
34
|
+
expect(r.get('test:graph')).toBe(a);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws on duplicate registration', () => {
|
|
38
|
+
const r = new AdapterRegistry();
|
|
39
|
+
r.register(fakeAdapter('test:graph'));
|
|
40
|
+
expect(() => r.register(fakeAdapter('test:graph'))).toThrow(/already registered/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('unregister removes the adapter', () => {
|
|
44
|
+
const r = new AdapterRegistry();
|
|
45
|
+
r.register(fakeAdapter('test:graph'));
|
|
46
|
+
expect(r.unregister('test:graph')).toBe(true);
|
|
47
|
+
expect(r.get('test:graph')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('list returns all adapters', () => {
|
|
51
|
+
const r = new AdapterRegistry();
|
|
52
|
+
r.register(fakeAdapter('a'));
|
|
53
|
+
r.register(fakeAdapter('b'));
|
|
54
|
+
expect(r.list()).toHaveLength(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('getRegistry returns a singleton', () => {
|
|
58
|
+
const r1 = getRegistry();
|
|
59
|
+
const r2 = getRegistry();
|
|
60
|
+
expect(r1).toBe(r2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('resetRegistry forces a new instance', () => {
|
|
64
|
+
const r1 = getRegistry();
|
|
65
|
+
resetRegistry();
|
|
66
|
+
const r2 = getRegistry();
|
|
67
|
+
expect(r1).not.toBe(r2);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Causal-Recovery Adapter Tests (Wedge 1, ADR-123 Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Acceptance:
|
|
5
|
+
* - Exports a SparseMatrix whose dimensions match the node-set
|
|
6
|
+
* - Result matrix is diagonally-dominant (coherence > 0)
|
|
7
|
+
* - Time-decay weights older events less
|
|
8
|
+
* - registerBrowserCausalAdapter() puts the adapter in the registry under
|
|
9
|
+
* the canonical `browser:causal:<origin>` graphId
|
|
10
|
+
* - sublinear/page-rank-entry routes through the adapter end-to-end
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
BrowserCausalAdapter,
|
|
16
|
+
browserCausalGraphId,
|
|
17
|
+
registerBrowserCausalAdapter,
|
|
18
|
+
type BreakEventLike,
|
|
19
|
+
type BreakEventSource,
|
|
20
|
+
} from '../src/adapters/browser-causal-adapter.js';
|
|
21
|
+
import { resetRegistry, getRegistry } from '../src/domain/adapter.js';
|
|
22
|
+
import { coherenceScore } from '../src/infrastructure/solver-bridge.js';
|
|
23
|
+
import { graphIntelligenceTools } from '../src/mcp-tools/index.js';
|
|
24
|
+
|
|
25
|
+
function stubSource(events: BreakEventLike[]): BreakEventSource {
|
|
26
|
+
return {
|
|
27
|
+
async listBreaks(origin) {
|
|
28
|
+
return events.filter((e) => e.origin === origin);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function evt(partial: Partial<BreakEventLike> & Pick<BreakEventLike, 'id' | 'selector' | 'origin'>): BreakEventLike {
|
|
34
|
+
return {
|
|
35
|
+
timestamp: '2026-05-19T00:00:00.000Z',
|
|
36
|
+
...partial,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('browserCausalGraphId', () => {
|
|
41
|
+
it('encodes origin into a stable identifier', () => {
|
|
42
|
+
expect(browserCausalGraphId('https://example.com')).toBe('browser:causal:https://example.com');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('BrowserCausalAdapter.exportAsSparseMatrix', () => {
|
|
47
|
+
it('returns an empty matrix when there are no break events', async () => {
|
|
48
|
+
const adapter = new BrowserCausalAdapter({
|
|
49
|
+
origin: 'https://empty.test',
|
|
50
|
+
source: stubSource([]),
|
|
51
|
+
});
|
|
52
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
53
|
+
expect(m.size).toBe(0);
|
|
54
|
+
expect(m.entries).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('builds a DD matrix from a chronological event sequence', async () => {
|
|
58
|
+
const adapter = new BrowserCausalAdapter({
|
|
59
|
+
origin: 'https://example.com',
|
|
60
|
+
source: stubSource([
|
|
61
|
+
evt({ id: 'b1', origin: 'https://example.com', selector: '@e1', timestamp: '2026-05-19T00:00:00.000Z' }),
|
|
62
|
+
evt({ id: 'b2', origin: 'https://example.com', selector: '@e2', timestamp: '2026-05-19T00:00:01.000Z' }),
|
|
63
|
+
evt({ id: 'b3', origin: 'https://example.com', selector: '@e3', timestamp: '2026-05-19T00:00:02.000Z' }),
|
|
64
|
+
]),
|
|
65
|
+
});
|
|
66
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
67
|
+
expect(m.size).toBe(3);
|
|
68
|
+
expect(coherenceScore(m)).toBeGreaterThan(0);
|
|
69
|
+
expect(m.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('honours nodeFilter to prune irrelevant rows', async () => {
|
|
73
|
+
const adapter = new BrowserCausalAdapter({
|
|
74
|
+
origin: 'https://example.com',
|
|
75
|
+
source: stubSource([
|
|
76
|
+
evt({ id: 'b1', origin: 'https://example.com', selector: '@e1' }),
|
|
77
|
+
evt({ id: 'b2', origin: 'https://example.com', selector: '@e2' }),
|
|
78
|
+
evt({ id: 'b3', origin: 'https://example.com', selector: '@e3' }),
|
|
79
|
+
]),
|
|
80
|
+
});
|
|
81
|
+
const m = await adapter.exportAsSparseMatrix({ nodeFilter: new Set(['@e1', '@e2']) });
|
|
82
|
+
expect(m.size).toBe(2);
|
|
83
|
+
expect(Object.keys(m.nodeIndex).sort()).toEqual(['@e1', '@e2']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('records (role:name) fuzzy keys when present', async () => {
|
|
87
|
+
const adapter = new BrowserCausalAdapter({
|
|
88
|
+
origin: 'https://example.com',
|
|
89
|
+
source: stubSource([
|
|
90
|
+
evt({
|
|
91
|
+
id: 'b1',
|
|
92
|
+
origin: 'https://example.com',
|
|
93
|
+
selector: '@e3',
|
|
94
|
+
lastKnownRole: 'button',
|
|
95
|
+
lastKnownName: 'Submit',
|
|
96
|
+
}),
|
|
97
|
+
evt({
|
|
98
|
+
id: 'b2',
|
|
99
|
+
origin: 'https://example.com',
|
|
100
|
+
selector: '@e3',
|
|
101
|
+
lastKnownRole: 'button',
|
|
102
|
+
lastKnownName: 'Submit',
|
|
103
|
+
}),
|
|
104
|
+
]),
|
|
105
|
+
});
|
|
106
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
107
|
+
expect(Object.keys(m.nodeIndex)).toContain('role:button:Submit');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('time-decays older events relative to newer ones', async () => {
|
|
111
|
+
const oneHour = 60 * 60 * 1000;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const events: BreakEventLike[] = [
|
|
114
|
+
evt({ id: 'old1', origin: 'https://x.com', selector: 'a', timestamp: new Date(now - 240 * oneHour).toISOString() }),
|
|
115
|
+
evt({ id: 'old2', origin: 'https://x.com', selector: 'b', timestamp: new Date(now - 239 * oneHour).toISOString() }),
|
|
116
|
+
evt({ id: 'new1', origin: 'https://x.com', selector: 'a', timestamp: new Date(now - oneHour).toISOString() }),
|
|
117
|
+
evt({ id: 'new2', origin: 'https://x.com', selector: 'b', timestamp: new Date(now).toISOString() }),
|
|
118
|
+
];
|
|
119
|
+
const adapter = new BrowserCausalAdapter({
|
|
120
|
+
origin: 'https://x.com',
|
|
121
|
+
source: stubSource(events),
|
|
122
|
+
halfLifeMs: 24 * oneHour,
|
|
123
|
+
});
|
|
124
|
+
const m = await adapter.exportAsSparseMatrix();
|
|
125
|
+
// Find the a→b edge: at least one entry should exist; the weight should
|
|
126
|
+
// be dominated by the recent event-pair, not the old one.
|
|
127
|
+
const aIdx = m.nodeIndex['a'];
|
|
128
|
+
const bIdx = m.nodeIndex['b'];
|
|
129
|
+
const offDiag = m.entries.filter((e) => e.row === aIdx && e.col === bIdx);
|
|
130
|
+
expect(offDiag.length).toBeGreaterThan(0);
|
|
131
|
+
// The accumulated decay should be small for old pair, ~1.0 for recent.
|
|
132
|
+
const totalWeight = offDiag.reduce((s, e) => s + e.value, 0);
|
|
133
|
+
// Recent pair contributes ~1.0, old pair contributes ~ exp(-239/24) ≈ 4e-5.
|
|
134
|
+
expect(totalWeight).toBeGreaterThan(0.5);
|
|
135
|
+
expect(totalWeight).toBeLessThan(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('registerBrowserCausalAdapter', () => {
|
|
140
|
+
beforeEach(() => resetRegistry());
|
|
141
|
+
|
|
142
|
+
it('registers under the canonical graph id', () => {
|
|
143
|
+
const registry = getRegistry();
|
|
144
|
+
registerBrowserCausalAdapter({
|
|
145
|
+
origin: 'https://example.com',
|
|
146
|
+
source: stubSource([]),
|
|
147
|
+
registry,
|
|
148
|
+
});
|
|
149
|
+
expect(registry.get('browser:causal:https://example.com')).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('end-to-end: sublinear/page-rank-entry resolves through the registered adapter', async () => {
|
|
153
|
+
const registry = getRegistry();
|
|
154
|
+
registerBrowserCausalAdapter({
|
|
155
|
+
origin: 'https://example.com',
|
|
156
|
+
source: stubSource([
|
|
157
|
+
evt({ id: 'b1', origin: 'https://example.com', selector: '@e1' }),
|
|
158
|
+
evt({ id: 'b2', origin: 'https://example.com', selector: '@e2' }),
|
|
159
|
+
evt({ id: 'b3', origin: 'https://example.com', selector: '@e3' }),
|
|
160
|
+
]),
|
|
161
|
+
registry,
|
|
162
|
+
});
|
|
163
|
+
const tool = graphIntelligenceTools.find((t) => t.name === 'sublinear/page-rank-entry');
|
|
164
|
+
expect(tool).toBeDefined();
|
|
165
|
+
const r = (await tool!.handler({
|
|
166
|
+
graphId: 'browser:causal:https://example.com',
|
|
167
|
+
nodeId: '@e2',
|
|
168
|
+
maxComplexityClass: 'polynomial',
|
|
169
|
+
})) as { success: boolean; result?: { score: number; complexityClass: string } };
|
|
170
|
+
expect(r.success).toBe(true);
|
|
171
|
+
expect(r.result?.score).toBeGreaterThanOrEqual(0);
|
|
172
|
+
expect(r.result?.complexityClass).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
});
|