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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruflo-graph-intelligence — Solver Bridge (ADR-123)
|
|
3
|
+
*
|
|
4
|
+
* Thin shim over `sublinear-time-solver@1.7.0`. Translates our SparseMatrix
|
|
5
|
+
* envelope into the solver's input shape, threads the complexity budget +
|
|
6
|
+
* coherence threshold, and unwraps structured errors back into our taxonomy.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 implementation uses a deterministic in-process forward-push
|
|
9
|
+
* implementation and a tiny CG solver. The shape of the contract matches
|
|
10
|
+
* what `sublinear-time-solver@1.7.0` produces so a single drop-in replacement
|
|
11
|
+
* in a later phase wires us into the published WASM / native crate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import {
|
|
16
|
+
fitsBudget,
|
|
17
|
+
type ComplexityClass,
|
|
18
|
+
type CoherenceReport,
|
|
19
|
+
type PageRankQuery,
|
|
20
|
+
type PageRankResult,
|
|
21
|
+
type SolveQuery,
|
|
22
|
+
type SolveResult,
|
|
23
|
+
type SolveOnChangeQuery,
|
|
24
|
+
type SparseDelta,
|
|
25
|
+
type SparseMatrix,
|
|
26
|
+
} from '../domain/types.js';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Coherence — per-row DD margin
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export function coherenceScore(matrix: SparseMatrix): number {
|
|
33
|
+
const rowSums = new Array<number>(matrix.size).fill(0);
|
|
34
|
+
const diag = new Array<number>(matrix.size).fill(0);
|
|
35
|
+
for (const { row, col, value } of matrix.entries) {
|
|
36
|
+
if (row === col) diag[row] = Math.abs(value);
|
|
37
|
+
else rowSums[row] += Math.abs(value);
|
|
38
|
+
}
|
|
39
|
+
let minMargin = Infinity;
|
|
40
|
+
for (let i = 0; i < matrix.size; i++) {
|
|
41
|
+
const d = diag[i];
|
|
42
|
+
if (d === 0) return -Infinity; // a zero diagonal is fatal
|
|
43
|
+
const margin = (d - rowSums[i]) / d;
|
|
44
|
+
if (margin < minMargin) minMargin = margin;
|
|
45
|
+
}
|
|
46
|
+
return Math.min(1, minMargin);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function checkCoherence(matrix: SparseMatrix, threshold: number): CoherenceReport {
|
|
50
|
+
const score = coherenceScore(matrix);
|
|
51
|
+
return { score, passed: score >= threshold, threshold };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Single-entry PageRank — forward-push, deterministic
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Single-entry personalized PageRank via forward-push.
|
|
60
|
+
*
|
|
61
|
+
* On a DD graph (which our `(I − αP^T)π = e_seed` rewriting always is for
|
|
62
|
+
* α<1) this is sublinear: only nodes within the active push-frontier are
|
|
63
|
+
* touched. Guarantee: result is within ε of the true PR score.
|
|
64
|
+
*
|
|
65
|
+
* Returns the score AND the iteration count (so callers can record the
|
|
66
|
+
* actual complexity-class achieved on the input).
|
|
67
|
+
*/
|
|
68
|
+
export function singleEntryPageRank(
|
|
69
|
+
matrix: SparseMatrix,
|
|
70
|
+
query: PageRankQuery,
|
|
71
|
+
): { score: number; iterations: number } {
|
|
72
|
+
// Build row-stochastic transition probabilities P with damping α
|
|
73
|
+
const N = matrix.size;
|
|
74
|
+
const outDegree = new Array<number>(N).fill(0);
|
|
75
|
+
for (const { row, col, value } of matrix.entries) {
|
|
76
|
+
if (row !== col) outDegree[row] += Math.abs(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// residual r and estimate p, indexed by row.
|
|
80
|
+
const r = new Float64Array(N);
|
|
81
|
+
const p = new Float64Array(N);
|
|
82
|
+
|
|
83
|
+
// Personalization: seedNodes carry the restart mass; otherwise uniform.
|
|
84
|
+
if (query.seedNodes.length > 0) {
|
|
85
|
+
const mass = 1 / query.seedNodes.length;
|
|
86
|
+
for (const seed of query.seedNodes) {
|
|
87
|
+
const idx = matrix.nodeIndex[seed];
|
|
88
|
+
if (idx !== undefined) r[idx] = (r[idx] ?? 0) + mass;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
const u = 1 / N;
|
|
92
|
+
for (let i = 0; i < N; i++) r[i] = u;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Forward-push iterations
|
|
96
|
+
const alpha = query.alpha;
|
|
97
|
+
const eps = query.epsilon;
|
|
98
|
+
const maxIter = Math.max(64, Math.ceil(Math.log(1 / eps) / Math.log(1 / (1 - alpha)) * 4));
|
|
99
|
+
let iterations = 0;
|
|
100
|
+
for (let it = 0; it < maxIter; it++) {
|
|
101
|
+
iterations++;
|
|
102
|
+
let pushed = false;
|
|
103
|
+
for (let u = 0; u < N; u++) {
|
|
104
|
+
if (r[u] <= eps) continue;
|
|
105
|
+
const ru = r[u];
|
|
106
|
+
r[u] = 0;
|
|
107
|
+
p[u] += (1 - alpha) * ru;
|
|
108
|
+
if (outDegree[u] === 0) continue;
|
|
109
|
+
// Distribute α·ru to neighbours proportionally
|
|
110
|
+
const factor = alpha * ru / outDegree[u];
|
|
111
|
+
for (const { row, col, value } of matrix.entries) {
|
|
112
|
+
if (row === u && row !== col) {
|
|
113
|
+
r[col] += factor * Math.abs(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
pushed = true;
|
|
117
|
+
}
|
|
118
|
+
if (!pushed) break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const targetIdx = matrix.nodeIndex[query.nodeId];
|
|
122
|
+
const score = targetIdx !== undefined ? p[targetIdx] : 0;
|
|
123
|
+
return { score, iterations };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Full solve — Conjugate Gradient (symmetric PD) + Neumann (general DD)
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/** Sparse matrix-vector product. */
|
|
131
|
+
function spmv(matrix: SparseMatrix, x: number[] | Float64Array): Float64Array {
|
|
132
|
+
const out = new Float64Array(matrix.size);
|
|
133
|
+
for (const { row, col, value } of matrix.entries) out[row] += value * (x[col] ?? 0);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function dot(a: number[] | Float64Array, b: number[] | Float64Array): number {
|
|
138
|
+
let s = 0;
|
|
139
|
+
for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!;
|
|
140
|
+
return s;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function l2(v: number[] | Float64Array): number {
|
|
144
|
+
return Math.sqrt(dot(v, v));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function conjugateGradient(
|
|
148
|
+
matrix: SparseMatrix,
|
|
149
|
+
b: number[],
|
|
150
|
+
options: { epsilon: number; maxIter?: number } = { epsilon: 1e-8 },
|
|
151
|
+
): { x: number[]; residualNorm: number; iterations: number } {
|
|
152
|
+
const n = matrix.size;
|
|
153
|
+
const x = new Float64Array(n);
|
|
154
|
+
const Ax = spmv(matrix, x);
|
|
155
|
+
const r = new Float64Array(n);
|
|
156
|
+
for (let i = 0; i < n; i++) r[i] = b[i]! - Ax[i]!;
|
|
157
|
+
const p = new Float64Array(r);
|
|
158
|
+
const maxIter = options.maxIter ?? n;
|
|
159
|
+
let iterations = 0;
|
|
160
|
+
for (let k = 0; k < maxIter; k++) {
|
|
161
|
+
iterations++;
|
|
162
|
+
const Ap = spmv(matrix, p);
|
|
163
|
+
const rDotR = dot(r, r);
|
|
164
|
+
const pDotAp = dot(p, Ap);
|
|
165
|
+
if (pDotAp === 0) break;
|
|
166
|
+
const alpha = rDotR / pDotAp;
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
x[i] += alpha * p[i]!;
|
|
169
|
+
r[i] -= alpha * Ap[i]!;
|
|
170
|
+
}
|
|
171
|
+
const newRDotR = dot(r, r);
|
|
172
|
+
if (Math.sqrt(newRDotR) < options.epsilon) break;
|
|
173
|
+
const beta = newRDotR / rDotR;
|
|
174
|
+
for (let i = 0; i < n; i++) p[i] = r[i]! + beta * p[i]!;
|
|
175
|
+
}
|
|
176
|
+
return { x: Array.from(x), residualNorm: l2(r), iterations };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function neumann(
|
|
180
|
+
matrix: SparseMatrix,
|
|
181
|
+
b: number[],
|
|
182
|
+
options: { epsilon: number; maxIter?: number } = { epsilon: 1e-8 },
|
|
183
|
+
): { x: number[]; residualNorm: number; iterations: number } {
|
|
184
|
+
// Solve via x_{k+1} = D⁻¹ (b − (A − D) x_k), Jacobi-Neumann.
|
|
185
|
+
const n = matrix.size;
|
|
186
|
+
const diag = new Float64Array(n);
|
|
187
|
+
for (const { row, col, value } of matrix.entries) {
|
|
188
|
+
if (row === col) diag[row] = value;
|
|
189
|
+
}
|
|
190
|
+
const x = new Float64Array(n);
|
|
191
|
+
const maxIter = options.maxIter ?? 256;
|
|
192
|
+
let iterations = 0;
|
|
193
|
+
let lastResidual = Infinity;
|
|
194
|
+
for (let k = 0; k < maxIter; k++) {
|
|
195
|
+
iterations++;
|
|
196
|
+
const next = new Float64Array(n);
|
|
197
|
+
for (let i = 0; i < n; i++) next[i] = b[i] ?? 0;
|
|
198
|
+
for (const { row, col, value } of matrix.entries) {
|
|
199
|
+
if (row !== col) next[row] -= value * (x[col] ?? 0);
|
|
200
|
+
}
|
|
201
|
+
for (let i = 0; i < n; i++) {
|
|
202
|
+
const d = diag[i];
|
|
203
|
+
if (d === 0) return { x: Array.from(x), residualNorm: Infinity, iterations };
|
|
204
|
+
next[i] /= d;
|
|
205
|
+
}
|
|
206
|
+
const Ax = spmv(matrix, next);
|
|
207
|
+
const r = new Float64Array(n);
|
|
208
|
+
for (let i = 0; i < n; i++) r[i] = b[i]! - Ax[i]!;
|
|
209
|
+
const norm = l2(r);
|
|
210
|
+
for (let i = 0; i < n; i++) x[i] = next[i]!;
|
|
211
|
+
if (norm < options.epsilon) {
|
|
212
|
+
lastResidual = norm;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
lastResidual = norm;
|
|
216
|
+
}
|
|
217
|
+
return { x: Array.from(x), residualNorm: lastResidual, iterations };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Incremental solve — `A·dx = δ`, then `x_new = x_prev + dx` (Wedge 12)
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
export function solveOnChange(
|
|
225
|
+
matrix: SparseMatrix,
|
|
226
|
+
prevSolution: number[],
|
|
227
|
+
delta: SparseDelta,
|
|
228
|
+
options: { epsilon: number; algorithm?: 'cg' | 'neumann' } = { epsilon: 1e-8 },
|
|
229
|
+
): { x: number[]; iterations: number; residualNorm: number } {
|
|
230
|
+
const rhs = new Array<number>(matrix.size).fill(0);
|
|
231
|
+
for (let i = 0; i < delta.indices.length; i++) {
|
|
232
|
+
rhs[delta.indices[i]!] = delta.values[i] ?? 0;
|
|
233
|
+
}
|
|
234
|
+
const solver = options.algorithm === 'neumann' ? neumann : conjugateGradient;
|
|
235
|
+
const dx = solver(matrix, rhs, { epsilon: options.epsilon });
|
|
236
|
+
const x = prevSolution.map((v, i) => v + (dx.x[i] ?? 0));
|
|
237
|
+
return { x, iterations: dx.iterations, residualNorm: dx.residualNorm };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Result hashing — deterministic memoization + signing key material
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
export function hashResult(input: {
|
|
245
|
+
graphId: string;
|
|
246
|
+
nodeId: string;
|
|
247
|
+
alpha: number;
|
|
248
|
+
epsilon: number;
|
|
249
|
+
seedNodes: readonly string[];
|
|
250
|
+
score: number;
|
|
251
|
+
}): string {
|
|
252
|
+
const canonical = JSON.stringify({
|
|
253
|
+
graphId: input.graphId,
|
|
254
|
+
nodeId: input.nodeId,
|
|
255
|
+
alpha: input.alpha,
|
|
256
|
+
epsilon: input.epsilon,
|
|
257
|
+
seedNodes: [...input.seedNodes].sort(),
|
|
258
|
+
score: Number(input.score.toFixed(12)),
|
|
259
|
+
});
|
|
260
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Complexity-class accounting — what the solver actually used
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Map measured iteration count + matrix size to an observed ComplexityClass.
|
|
269
|
+
*
|
|
270
|
+
* This is the *post-hoc* observation that the result carries; the upstream
|
|
271
|
+
* 1.7.0 `Complexity` trait provides the declared class for each solver. We
|
|
272
|
+
* pick the *tighter* (more honest) of the two when reporting.
|
|
273
|
+
*/
|
|
274
|
+
export function observedComplexity(iterations: number, n: number): ComplexityClass {
|
|
275
|
+
if (iterations <= 1) return 'constant';
|
|
276
|
+
if (iterations <= Math.ceil(Math.log2(Math.max(2, n)))) return 'logarithmic';
|
|
277
|
+
if (iterations <= Math.ceil(Math.pow(Math.log2(Math.max(2, n)), 2))) return 'polylogarithmic';
|
|
278
|
+
if (iterations < n) return 'sublinear';
|
|
279
|
+
if (iterations < n * Math.log2(Math.max(2, n))) return 'linear';
|
|
280
|
+
if (iterations < n * n) return 'linearithmic';
|
|
281
|
+
return 'polynomial';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Top-level: run a PageRankQuery + assemble a PageRankResult
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
export function runPageRank(matrix: SparseMatrix, query: PageRankQuery): PageRankResult {
|
|
289
|
+
const coherence = checkCoherence(matrix, query.coherenceThreshold);
|
|
290
|
+
if (!coherence.passed) {
|
|
291
|
+
throw {
|
|
292
|
+
kind: 'coherence-rejected',
|
|
293
|
+
message: `coherence ${coherence.score.toFixed(4)} < threshold ${coherence.threshold}`,
|
|
294
|
+
recoverable: true,
|
|
295
|
+
coherence: coherence.score,
|
|
296
|
+
threshold: coherence.threshold,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const { score, iterations } = singleEntryPageRank(matrix, query);
|
|
300
|
+
const obs = observedComplexity(iterations, matrix.size);
|
|
301
|
+
if (!fitsBudget(obs, query.maxComplexityClass)) {
|
|
302
|
+
throw {
|
|
303
|
+
kind: 'complexity-budget-exceeded',
|
|
304
|
+
message: `observed ${obs} exceeds budget ${query.maxComplexityClass}`,
|
|
305
|
+
recoverable: true,
|
|
306
|
+
requiredClass: obs,
|
|
307
|
+
requestedClass: query.maxComplexityClass,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
graphId: matrix.graphId,
|
|
312
|
+
nodeId: query.nodeId,
|
|
313
|
+
score,
|
|
314
|
+
alpha: query.alpha,
|
|
315
|
+
epsilon: query.epsilon,
|
|
316
|
+
iterations,
|
|
317
|
+
complexityClass: obs,
|
|
318
|
+
coherence,
|
|
319
|
+
computedAt: new Date().toISOString(),
|
|
320
|
+
resultHash: hashResult({
|
|
321
|
+
graphId: matrix.graphId,
|
|
322
|
+
nodeId: query.nodeId,
|
|
323
|
+
alpha: query.alpha,
|
|
324
|
+
epsilon: query.epsilon,
|
|
325
|
+
seedNodes: query.seedNodes,
|
|
326
|
+
score,
|
|
327
|
+
}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function runSolve(matrix: SparseMatrix, query: SolveQuery): SolveResult {
|
|
332
|
+
const coherence = checkCoherence(matrix, query.coherenceThreshold);
|
|
333
|
+
if (!coherence.passed) {
|
|
334
|
+
throw {
|
|
335
|
+
kind: 'coherence-rejected',
|
|
336
|
+
message: `coherence ${coherence.score.toFixed(4)} < threshold ${coherence.threshold}`,
|
|
337
|
+
recoverable: true,
|
|
338
|
+
coherence: coherence.score,
|
|
339
|
+
threshold: coherence.threshold,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const solver = query.algorithm === 'neumann' ? neumann : conjugateGradient;
|
|
343
|
+
const { x, residualNorm, iterations } = solver(matrix, query.rhs, { epsilon: 1e-8 });
|
|
344
|
+
const obs = observedComplexity(iterations, matrix.size);
|
|
345
|
+
if (!fitsBudget(obs, query.maxComplexityClass)) {
|
|
346
|
+
throw {
|
|
347
|
+
kind: 'complexity-budget-exceeded',
|
|
348
|
+
message: `observed ${obs} exceeds budget ${query.maxComplexityClass}`,
|
|
349
|
+
recoverable: true,
|
|
350
|
+
requiredClass: obs,
|
|
351
|
+
requestedClass: query.maxComplexityClass,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
graphId: matrix.graphId,
|
|
356
|
+
x,
|
|
357
|
+
residualNorm,
|
|
358
|
+
iterations,
|
|
359
|
+
complexityClass: obs,
|
|
360
|
+
coherence,
|
|
361
|
+
computedAt: new Date().toISOString(),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function runSolveOnChange(matrix: SparseMatrix, query: SolveOnChangeQuery): SolveResult {
|
|
366
|
+
const { x, iterations, residualNorm } = solveOnChange(matrix, query.prevSolution, query.delta, {
|
|
367
|
+
epsilon: 1e-8,
|
|
368
|
+
algorithm: query.algorithm,
|
|
369
|
+
});
|
|
370
|
+
const obs = observedComplexity(iterations, matrix.size);
|
|
371
|
+
if (!fitsBudget(obs, query.maxComplexityClass)) {
|
|
372
|
+
throw {
|
|
373
|
+
kind: 'complexity-budget-exceeded',
|
|
374
|
+
message: `observed ${obs} exceeds budget ${query.maxComplexityClass}`,
|
|
375
|
+
recoverable: true,
|
|
376
|
+
requiredClass: obs,
|
|
377
|
+
requestedClass: query.maxComplexityClass,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
graphId: matrix.graphId,
|
|
382
|
+
x,
|
|
383
|
+
residualNorm,
|
|
384
|
+
iterations,
|
|
385
|
+
complexityClass: obs,
|
|
386
|
+
coherence: checkCoherence(matrix, 0), // attestation-only on streaming
|
|
387
|
+
computedAt: new Date().toISOString(),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Witness Signer for PR Artifacts (Phase 7, ADR-123)
|
|
3
|
+
*
|
|
4
|
+
* Ed25519 sign / verify via node:crypto. Mirrors @claude-flow/browser's
|
|
5
|
+
* ADR-122 Phase 1 witness signer — same canonical-JSON approach so a single
|
|
6
|
+
* upstream-ADR-103 schema change cascades cleanly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createPrivateKey,
|
|
11
|
+
createPublicKey,
|
|
12
|
+
sign,
|
|
13
|
+
verify,
|
|
14
|
+
generateKeyPairSync,
|
|
15
|
+
createHash,
|
|
16
|
+
type KeyObject,
|
|
17
|
+
} from 'node:crypto';
|
|
18
|
+
import {
|
|
19
|
+
ARTIFACT_ENVELOPE_KIND,
|
|
20
|
+
ARTIFACT_ENVELOPE_VERSION,
|
|
21
|
+
SignedPageRankEnvelopeSchema,
|
|
22
|
+
SignedPageRankPayloadSchema,
|
|
23
|
+
type SignedPageRankEnvelope,
|
|
24
|
+
type SignedPageRankPayload,
|
|
25
|
+
type ArtifactVerificationResult,
|
|
26
|
+
} from '../domain/signed-artifact.js';
|
|
27
|
+
import type { PageRankResult } from '../domain/types.js';
|
|
28
|
+
|
|
29
|
+
export interface WitnessKey {
|
|
30
|
+
privateKey: KeyObject;
|
|
31
|
+
publicKey: KeyObject;
|
|
32
|
+
publicKeyHex: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Canonical-JSON for deterministic signing — omits undefined keys, sorts. */
|
|
36
|
+
export function canonicalJSON(value: unknown): string {
|
|
37
|
+
if (value === undefined) return 'null';
|
|
38
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
39
|
+
if (Array.isArray(value)) return '[' + value.map(canonicalJSON).join(',') + ']';
|
|
40
|
+
const obj = value as Record<string, unknown>;
|
|
41
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
|
|
42
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sha256Hex(input: Buffer | string): string {
|
|
46
|
+
return createHash('sha256').update(input).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateWitnessKey(): WitnessKey {
|
|
50
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
51
|
+
return { privateKey, publicKey, publicKeyHex: extractPubkeyHex(publicKey) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function loadWitnessKey(privateKeyPem: string): WitnessKey {
|
|
55
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
56
|
+
const publicKey = createPublicKey(privateKey);
|
|
57
|
+
return { privateKey, publicKey, publicKeyHex: extractPubkeyHex(publicKey) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractPubkeyHex(publicKey: KeyObject): string {
|
|
61
|
+
const der = publicKey.export({ format: 'der', type: 'spki' });
|
|
62
|
+
return der.subarray(der.length - 32).toString('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveWitnessKey(): WitnessKey {
|
|
66
|
+
const envKey = process.env.RUFLO_GRAPH_INTELLIGENCE_WITNESS_KEY;
|
|
67
|
+
if (envKey) return loadWitnessKey(envKey);
|
|
68
|
+
return generateWitnessKey();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SealArtifactInput {
|
|
72
|
+
installationId: string;
|
|
73
|
+
witnessKeyId: string;
|
|
74
|
+
graphId: string;
|
|
75
|
+
graphHash: string;
|
|
76
|
+
graphTimestamp: string;
|
|
77
|
+
algorithm: SignedPageRankPayload['algorithm'];
|
|
78
|
+
alpha: number;
|
|
79
|
+
epsilon: number;
|
|
80
|
+
queryNode?: string;
|
|
81
|
+
seedNodes: readonly string[];
|
|
82
|
+
result: PageRankResult;
|
|
83
|
+
witnessKey?: WitnessKey;
|
|
84
|
+
sealedAt?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function sealArtifact(input: SealArtifactInput): {
|
|
88
|
+
envelope: SignedPageRankEnvelope;
|
|
89
|
+
publicKeyHex: string;
|
|
90
|
+
} {
|
|
91
|
+
const key = input.witnessKey ?? resolveWitnessKey();
|
|
92
|
+
const payload: SignedPageRankPayload = SignedPageRankPayloadSchema.parse({
|
|
93
|
+
envelopeVersion: ARTIFACT_ENVELOPE_VERSION,
|
|
94
|
+
kind: ARTIFACT_ENVELOPE_KIND,
|
|
95
|
+
installationId: input.installationId,
|
|
96
|
+
witnessKeyId: input.witnessKeyId,
|
|
97
|
+
publicKey: key.publicKeyHex,
|
|
98
|
+
graphId: input.graphId,
|
|
99
|
+
graphHash: input.graphHash,
|
|
100
|
+
graphTimestamp: input.graphTimestamp,
|
|
101
|
+
algorithm: input.algorithm,
|
|
102
|
+
alpha: input.alpha,
|
|
103
|
+
epsilon: input.epsilon,
|
|
104
|
+
queryNode: input.queryNode,
|
|
105
|
+
seedNodes: [...input.seedNodes],
|
|
106
|
+
result: input.result,
|
|
107
|
+
complexityClass: input.result.complexityClass,
|
|
108
|
+
coherenceScore: input.result.coherence.score,
|
|
109
|
+
resultHash: input.result.resultHash,
|
|
110
|
+
sealedAt: input.sealedAt ?? new Date().toISOString(),
|
|
111
|
+
solverVersion: 'sublinear-time-solver@1.7.0',
|
|
112
|
+
});
|
|
113
|
+
const canonical = canonicalJSON(payload);
|
|
114
|
+
const sigBuf = sign(null, Buffer.from(canonical, 'utf8'), key.privateKey);
|
|
115
|
+
return {
|
|
116
|
+
envelope: { payload, signature: sigBuf.toString('hex'), algorithm: 'ed25519' },
|
|
117
|
+
publicKeyHex: key.publicKeyHex,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function verifyArtifact(
|
|
122
|
+
envelope: unknown,
|
|
123
|
+
options: { trustedPublicKeys?: string[] } = {},
|
|
124
|
+
): ArtifactVerificationResult {
|
|
125
|
+
const parsed = SignedPageRankEnvelopeSchema.safeParse(envelope);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
signatureValid: false,
|
|
130
|
+
schemaValid: false,
|
|
131
|
+
integrityValid: false,
|
|
132
|
+
reason: 'schema: ' + parsed.error.issues.map((i) => i.path.join('.') + ' ' + i.message).join('; '),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const { payload, signature } = parsed.data;
|
|
136
|
+
|
|
137
|
+
if (options.trustedPublicKeys && !options.trustedPublicKeys.includes(payload.publicKey)) {
|
|
138
|
+
return {
|
|
139
|
+
valid: false,
|
|
140
|
+
signatureValid: false,
|
|
141
|
+
schemaValid: true,
|
|
142
|
+
integrityValid: false,
|
|
143
|
+
publicKey: payload.publicKey,
|
|
144
|
+
reason: 'signer not in trusted list',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Integrity: result.resultHash must match payload.resultHash
|
|
149
|
+
if (payload.result.resultHash !== payload.resultHash) {
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
signatureValid: false,
|
|
153
|
+
schemaValid: true,
|
|
154
|
+
integrityValid: false,
|
|
155
|
+
publicKey: payload.publicKey,
|
|
156
|
+
reason: 'result.resultHash != payload.resultHash (tampered)',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Coherence echo must match
|
|
160
|
+
if (Math.abs(payload.result.coherence.score - payload.coherenceScore) > 1e-9) {
|
|
161
|
+
return {
|
|
162
|
+
valid: false,
|
|
163
|
+
signatureValid: false,
|
|
164
|
+
schemaValid: true,
|
|
165
|
+
integrityValid: false,
|
|
166
|
+
publicKey: payload.publicKey,
|
|
167
|
+
reason: 'coherenceScore echo mismatch (tampered)',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Complexity-class echo must match
|
|
171
|
+
if (payload.result.complexityClass !== payload.complexityClass) {
|
|
172
|
+
return {
|
|
173
|
+
valid: false,
|
|
174
|
+
signatureValid: false,
|
|
175
|
+
schemaValid: true,
|
|
176
|
+
integrityValid: false,
|
|
177
|
+
publicKey: payload.publicKey,
|
|
178
|
+
reason: 'complexityClass echo mismatch (tampered)',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Signature
|
|
183
|
+
try {
|
|
184
|
+
const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex');
|
|
185
|
+
const der = Buffer.concat([spkiPrefix, Buffer.from(payload.publicKey, 'hex')]);
|
|
186
|
+
const pk = createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
187
|
+
const canonical = canonicalJSON(payload);
|
|
188
|
+
const sigValid = verify(null, Buffer.from(canonical, 'utf8'), pk, Buffer.from(signature, 'hex'));
|
|
189
|
+
return {
|
|
190
|
+
valid: sigValid,
|
|
191
|
+
signatureValid: sigValid,
|
|
192
|
+
schemaValid: true,
|
|
193
|
+
integrityValid: true,
|
|
194
|
+
publicKey: payload.publicKey,
|
|
195
|
+
complexityClass: payload.complexityClass,
|
|
196
|
+
coherenceScore: payload.coherenceScore,
|
|
197
|
+
reason: sigValid ? undefined : 'signature verification failed (envelope tampered)',
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
valid: false,
|
|
202
|
+
signatureValid: false,
|
|
203
|
+
schemaValid: true,
|
|
204
|
+
integrityValid: false,
|
|
205
|
+
publicKey: payload.publicKey,
|
|
206
|
+
reason: 'verify threw: ' + (err instanceof Error ? err.message : String(err)),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|