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.
Files changed (126) hide show
  1. package/.claude-flow/data/pending-insights.jsonl +30 -0
  2. package/dist/adapters/aidefence-suspicion-adapter.d.ts +40 -0
  3. package/dist/adapters/aidefence-suspicion-adapter.d.ts.map +1 -0
  4. package/dist/adapters/aidefence-suspicion-adapter.js +77 -0
  5. package/dist/adapters/aidefence-suspicion-adapter.js.map +1 -0
  6. package/dist/adapters/browser-causal-adapter.d.ts +83 -0
  7. package/dist/adapters/browser-causal-adapter.d.ts.map +1 -0
  8. package/dist/adapters/browser-causal-adapter.js +146 -0
  9. package/dist/adapters/browser-causal-adapter.js.map +1 -0
  10. package/dist/adapters/cost-attribution-adapter.d.ts +48 -0
  11. package/dist/adapters/cost-attribution-adapter.d.ts.map +1 -0
  12. package/dist/adapters/cost-attribution-adapter.js +95 -0
  13. package/dist/adapters/cost-attribution-adapter.js.map +1 -0
  14. package/dist/adapters/federation-trust-adapter.d.ts +49 -0
  15. package/dist/adapters/federation-trust-adapter.d.ts.map +1 -0
  16. package/dist/adapters/federation-trust-adapter.js +82 -0
  17. package/dist/adapters/federation-trust-adapter.js.map +1 -0
  18. package/dist/adapters/index.d.ts +16 -0
  19. package/dist/adapters/index.d.ts.map +1 -0
  20. package/dist/adapters/index.js +16 -0
  21. package/dist/adapters/index.js.map +1 -0
  22. package/dist/adapters/jujutsu-blast-radius-adapter.d.ts +46 -0
  23. package/dist/adapters/jujutsu-blast-radius-adapter.d.ts.map +1 -0
  24. package/dist/adapters/jujutsu-blast-radius-adapter.js +80 -0
  25. package/dist/adapters/jujutsu-blast-radius-adapter.js.map +1 -0
  26. package/dist/adapters/knowledge-graph-adapter.d.ts +41 -0
  27. package/dist/adapters/knowledge-graph-adapter.d.ts.map +1 -0
  28. package/dist/adapters/knowledge-graph-adapter.js +83 -0
  29. package/dist/adapters/knowledge-graph-adapter.js.map +1 -0
  30. package/dist/adapters/observability-span-adapter.d.ts +45 -0
  31. package/dist/adapters/observability-span-adapter.d.ts.map +1 -0
  32. package/dist/adapters/observability-span-adapter.js +97 -0
  33. package/dist/adapters/observability-span-adapter.js.map +1 -0
  34. package/dist/adapters/portfolio-cg-adapter.d.ts +60 -0
  35. package/dist/adapters/portfolio-cg-adapter.d.ts.map +1 -0
  36. package/dist/adapters/portfolio-cg-adapter.js +102 -0
  37. package/dist/adapters/portfolio-cg-adapter.js.map +1 -0
  38. package/dist/adapters/rag-memory-adapter.d.ts +49 -0
  39. package/dist/adapters/rag-memory-adapter.d.ts.map +1 -0
  40. package/dist/adapters/rag-memory-adapter.js +86 -0
  41. package/dist/adapters/rag-memory-adapter.js.map +1 -0
  42. package/dist/application/federation-client.d.ts +54 -0
  43. package/dist/application/federation-client.d.ts.map +1 -0
  44. package/dist/application/federation-client.js +101 -0
  45. package/dist/application/federation-client.js.map +1 -0
  46. package/dist/application/federation-server.d.ts +38 -0
  47. package/dist/application/federation-server.d.ts.map +1 -0
  48. package/dist/application/federation-server.js +127 -0
  49. package/dist/application/federation-server.js.map +1 -0
  50. package/dist/application/streaming-bridge.d.ts +62 -0
  51. package/dist/application/streaming-bridge.d.ts.map +1 -0
  52. package/dist/application/streaming-bridge.js +101 -0
  53. package/dist/application/streaming-bridge.js.map +1 -0
  54. package/dist/domain/adapter.d.ts +58 -0
  55. package/dist/domain/adapter.d.ts.map +1 -0
  56. package/dist/domain/adapter.js +43 -0
  57. package/dist/domain/adapter.js.map +1 -0
  58. package/dist/domain/federation-protocol.d.ts +857 -0
  59. package/dist/domain/federation-protocol.d.ts.map +1 -0
  60. package/dist/domain/federation-protocol.js +72 -0
  61. package/dist/domain/federation-protocol.js.map +1 -0
  62. package/dist/domain/signed-artifact.d.ts +429 -0
  63. package/dist/domain/signed-artifact.d.ts.map +1 -0
  64. package/dist/domain/signed-artifact.js +57 -0
  65. package/dist/domain/signed-artifact.js.map +1 -0
  66. package/dist/domain/types.d.ts +329 -0
  67. package/dist/domain/types.d.ts.map +1 -0
  68. package/dist/domain/types.js +165 -0
  69. package/dist/domain/types.js.map +1 -0
  70. package/dist/index.d.ts +31 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +37 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/infrastructure/jl-embed.d.ts +27 -0
  75. package/dist/infrastructure/jl-embed.d.ts.map +1 -0
  76. package/dist/infrastructure/jl-embed.js +79 -0
  77. package/dist/infrastructure/jl-embed.js.map +1 -0
  78. package/dist/infrastructure/solver-bridge.d.ts +73 -0
  79. package/dist/infrastructure/solver-bridge.d.ts.map +1 -0
  80. package/dist/infrastructure/solver-bridge.js +359 -0
  81. package/dist/infrastructure/solver-bridge.js.map +1 -0
  82. package/dist/infrastructure/witness-signer.d.ts +44 -0
  83. package/dist/infrastructure/witness-signer.d.ts.map +1 -0
  84. package/dist/infrastructure/witness-signer.js +158 -0
  85. package/dist/infrastructure/witness-signer.js.map +1 -0
  86. package/dist/mcp-tools/index.d.ts +27 -0
  87. package/dist/mcp-tools/index.d.ts.map +1 -0
  88. package/dist/mcp-tools/index.js +292 -0
  89. package/dist/mcp-tools/index.js.map +1 -0
  90. package/package.json +55 -0
  91. package/ruvector.db +0 -0
  92. package/src/adapters/aidefence-suspicion-adapter.ts +102 -0
  93. package/src/adapters/browser-causal-adapter.ts +193 -0
  94. package/src/adapters/cost-attribution-adapter.ts +123 -0
  95. package/src/adapters/federation-trust-adapter.ts +116 -0
  96. package/src/adapters/index.ts +87 -0
  97. package/src/adapters/jujutsu-blast-radius-adapter.ts +107 -0
  98. package/src/adapters/knowledge-graph-adapter.ts +110 -0
  99. package/src/adapters/observability-span-adapter.ts +123 -0
  100. package/src/adapters/portfolio-cg-adapter.ts +140 -0
  101. package/src/adapters/rag-memory-adapter.ts +117 -0
  102. package/src/application/federation-client.ts +147 -0
  103. package/src/application/federation-server.ts +158 -0
  104. package/src/application/streaming-bridge.ts +137 -0
  105. package/src/domain/adapter.ts +92 -0
  106. package/src/domain/federation-protocol.ts +95 -0
  107. package/src/domain/signed-artifact.ts +80 -0
  108. package/src/domain/types.ts +215 -0
  109. package/src/index.ts +105 -0
  110. package/src/infrastructure/jl-embed.ts +98 -0
  111. package/src/infrastructure/solver-bridge.ts +389 -0
  112. package/src/infrastructure/witness-signer.ts +209 -0
  113. package/src/mcp-tools/index.ts +316 -0
  114. package/tests/adapter-registry.test.ts +69 -0
  115. package/tests/browser-causal-adapter.test.ts +174 -0
  116. package/tests/mcp-tools.test.ts +169 -0
  117. package/tests/phase3-adapters.test.ts +206 -0
  118. package/tests/phase4-adapters.test.ts +158 -0
  119. package/tests/phase5-portfolio.test.ts +122 -0
  120. package/tests/phase6-adapters.test.ts +224 -0
  121. package/tests/phase6_5-streaming.test.ts +135 -0
  122. package/tests/phase7-signed-artifact.test.ts +238 -0
  123. package/tests/phase8-federation.test.ts +194 -0
  124. package/tests/solver-bridge.test.ts +255 -0
  125. package/tsconfig.json +21 -0
  126. package/vitest.config.ts +9 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Phase 8 Tests — Federation Distribution of Signed PR Vectors (beyond-SOTA)
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { FederationServer } from '../src/application/federation-server.js';
7
+ import {
8
+ FederationClient,
9
+ inProcessTransport,
10
+ } from '../src/application/federation-client.js';
11
+ import { generateWitnessKey } from '../src/infrastructure/witness-signer.js';
12
+ import { resetRegistry, getRegistry, type SublinearAdapter } from '../src/domain/adapter.js';
13
+ import type { SparseMatrix } from '../src/domain/types.js';
14
+ import { createHash } from 'node:crypto';
15
+
16
+ function ddAdapter(graphId: string, n = 6): SublinearAdapter {
17
+ const entries = [];
18
+ const nodeIndex: Record<string, number> = {};
19
+ const indexNode: string[] = [];
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
+ const contentHash = createHash('sha256')
28
+ .update(JSON.stringify({ graphId, n }))
29
+ .digest('hex');
30
+ const matrix: SparseMatrix = {
31
+ graphId,
32
+ size: n,
33
+ entries,
34
+ nodeIndex,
35
+ indexNode,
36
+ capturedAt: '2026-05-19T00:00:00Z',
37
+ contentHash,
38
+ };
39
+ return {
40
+ graphId,
41
+ ownerPlugin: 'test',
42
+ async exportAsSparseMatrix() {
43
+ return matrix;
44
+ },
45
+ };
46
+ }
47
+
48
+ describe('Federation round-trip', () => {
49
+ beforeEach(() => resetRegistry());
50
+
51
+ it('client receives a verifiable peer artifact', async () => {
52
+ const adapter = ddAdapter('fed:test', 6);
53
+ getRegistry().register(adapter);
54
+ const key = generateWitnessKey();
55
+ const server = new FederationServer({
56
+ installationId: 'inst-A',
57
+ witnessKey: key,
58
+ witnessKeyId: 'key-v1',
59
+ transport: { async send() { return null; }, onMessage() { return () => {}; } },
60
+ });
61
+ const client = new FederationClient({
62
+ installationId: 'inst-B',
63
+ transport: inProcessTransport(server),
64
+ trustedPublicKeys: [key.publicKeyHex],
65
+ });
66
+ const result = await client.fetchPageRank({
67
+ peer: 'inst-A',
68
+ graphId: 'fed:test',
69
+ nodeId: 'n2',
70
+ });
71
+ expect(result.origin).toBe('peer');
72
+ expect(result.verification?.valid).toBe(true);
73
+ expect(result.result.score).toBeGreaterThanOrEqual(0);
74
+ });
75
+
76
+ it('client falls back to local on untrusted signer', async () => {
77
+ const adapter = ddAdapter('fed:test', 6);
78
+ getRegistry().register(adapter);
79
+ const strangerKey = generateWitnessKey();
80
+ const server = new FederationServer({
81
+ installationId: 'inst-A',
82
+ witnessKey: strangerKey,
83
+ witnessKeyId: 'key-v1',
84
+ transport: { async send() { return null; }, onMessage() { return () => {}; } },
85
+ });
86
+ const client = new FederationClient({
87
+ installationId: 'inst-B',
88
+ transport: inProcessTransport(server),
89
+ trustedPublicKeys: ['0'.repeat(64)], // stranger NOT in trust list
90
+ });
91
+ const result = await client.fetchPageRank({
92
+ peer: 'inst-A',
93
+ graphId: 'fed:test',
94
+ nodeId: 'n2',
95
+ });
96
+ expect(result.origin).toBe('untrusted-fallback');
97
+ expect(result.fallbackReason).toMatch(/trusted list/i);
98
+ expect(result.result.score).toBeGreaterThanOrEqual(0);
99
+ });
100
+
101
+ it('server sends pr_artifact_stale when graphHash differs', async () => {
102
+ const adapter = ddAdapter('fed:test', 6);
103
+ getRegistry().register(adapter);
104
+ const key = generateWitnessKey();
105
+ const server = new FederationServer({
106
+ installationId: 'inst-A',
107
+ witnessKey: key,
108
+ witnessKeyId: 'key-v1',
109
+ transport: { async send() { return null; }, onMessage() { return () => {}; } },
110
+ });
111
+ const client = new FederationClient({
112
+ installationId: 'inst-B',
113
+ transport: inProcessTransport(server),
114
+ trustedPublicKeys: [key.publicKeyHex],
115
+ });
116
+ const result = await client.fetchPageRank({
117
+ peer: 'inst-A',
118
+ graphId: 'fed:test',
119
+ nodeId: 'n2',
120
+ lastKnownGraphHash: '0'.repeat(64), // doesn't match what server has
121
+ });
122
+ expect(result.origin).toBe('stale-fallback');
123
+ expect(result.fallbackReason).toMatch(/changed/);
124
+ });
125
+
126
+ it('server returns stale-reject for an unknown graphId', async () => {
127
+ const key = generateWitnessKey();
128
+ const server = new FederationServer({
129
+ installationId: 'inst-A',
130
+ witnessKey: key,
131
+ witnessKeyId: 'key-v1',
132
+ transport: { async send() { return null; }, onMessage() { return () => {}; } },
133
+ });
134
+ const client = new FederationClient({
135
+ installationId: 'inst-B',
136
+ transport: inProcessTransport(server),
137
+ });
138
+ // No adapter registered on client OR server side
139
+ try {
140
+ await client.fetchPageRank({
141
+ peer: 'inst-A',
142
+ graphId: 'fed:nonexistent',
143
+ nodeId: 'n2',
144
+ });
145
+ throw new Error('expected localCompute to throw');
146
+ } catch (err) {
147
+ expect((err as Error).message).toMatch(/no adapter/);
148
+ }
149
+ });
150
+
151
+ it('server rate-limits per peer', async () => {
152
+ const adapter = ddAdapter('fed:test', 6);
153
+ getRegistry().register(adapter);
154
+ const key = generateWitnessKey();
155
+ const server = new FederationServer({
156
+ installationId: 'inst-A',
157
+ witnessKey: key,
158
+ witnessKeyId: 'key-v1',
159
+ transport: { async send() { return null; }, onMessage() { return () => {}; } },
160
+ rateLimitPerPeerPerMinute: 2,
161
+ });
162
+ const client = new FederationClient({
163
+ installationId: 'inst-B',
164
+ transport: inProcessTransport(server),
165
+ trustedPublicKeys: [key.publicKeyHex],
166
+ });
167
+ const r1 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n2' });
168
+ const r2 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n3' });
169
+ const r3 = await client.fetchPageRank({ peer: 'inst-A', graphId: 'fed:test', nodeId: 'n4' });
170
+ expect(r1.origin).toBe('peer');
171
+ expect(r2.origin).toBe('peer');
172
+ expect(r3.origin).toBe('stale-fallback');
173
+ expect(r3.fallbackReason).toMatch(/rate limit/i);
174
+ });
175
+
176
+ it('client falls back to local compute when no usable response', async () => {
177
+ const adapter = ddAdapter('fed:test', 6);
178
+ getRegistry().register(adapter);
179
+ const client = new FederationClient({
180
+ installationId: 'inst-B',
181
+ transport: {
182
+ async send() { return null; }, // server unreachable
183
+ onMessage() { return () => {}; },
184
+ },
185
+ });
186
+ const result = await client.fetchPageRank({
187
+ peer: 'inst-A',
188
+ graphId: 'fed:test',
189
+ nodeId: 'n2',
190
+ });
191
+ expect(result.origin).toBe('local-fallback');
192
+ expect(result.result.score).toBeGreaterThanOrEqual(0);
193
+ });
194
+ });
@@ -0,0 +1,255 @@
1
+ /**
2
+ * ruflo-graph-intelligence — Solver Bridge Tests (ADR-123 Phase 1)
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ coherenceScore,
8
+ checkCoherence,
9
+ singleEntryPageRank,
10
+ conjugateGradient,
11
+ neumann,
12
+ solveOnChange,
13
+ observedComplexity,
14
+ hashResult,
15
+ runPageRank,
16
+ } from '../src/infrastructure/solver-bridge.js';
17
+ import { fitsBudget, isEdgeSafe, type SparseMatrix, type PageRankQuery } from '../src/domain/types.js';
18
+
19
+ /** Build a small DD matrix for tests. */
20
+ function ddMatrix(n: number): SparseMatrix {
21
+ const entries = [];
22
+ const nodeIndex: Record<string, number> = {};
23
+ const indexNode: string[] = [];
24
+ for (let i = 0; i < n; i++) {
25
+ nodeIndex[`n${i}`] = i;
26
+ indexNode.push(`n${i}`);
27
+ entries.push({ row: i, col: i, value: 5 });
28
+ if (i > 0) entries.push({ row: i, col: i - 1, value: -1 });
29
+ if (i < n - 1) entries.push({ row: i, col: i + 1, value: -1 });
30
+ }
31
+ return {
32
+ graphId: `test-dd-${n}`,
33
+ size: n,
34
+ entries,
35
+ nodeIndex,
36
+ indexNode,
37
+ capturedAt: '2026-05-19T00:00:00Z',
38
+ };
39
+ }
40
+
41
+ describe('coherence', () => {
42
+ it('reports positive coherence for a clean DD matrix', () => {
43
+ const m = ddMatrix(8);
44
+ const score = coherenceScore(m);
45
+ expect(score).toBeGreaterThan(0);
46
+ expect(score).toBeLessThanOrEqual(1);
47
+ });
48
+
49
+ it('passes the gate when threshold is below score', () => {
50
+ const m = ddMatrix(8);
51
+ const r = checkCoherence(m, 0.1);
52
+ expect(r.passed).toBe(true);
53
+ expect(r.score).toBeGreaterThan(0.1);
54
+ });
55
+
56
+ it('rejects when threshold exceeds score', () => {
57
+ const m = ddMatrix(8);
58
+ const r = checkCoherence(m, 0.99);
59
+ expect(r.passed).toBe(false);
60
+ });
61
+
62
+ it('reports −∞ for a zero-diagonal matrix', () => {
63
+ const m: SparseMatrix = {
64
+ graphId: 'singular',
65
+ size: 2,
66
+ entries: [{ row: 0, col: 1, value: 1 }],
67
+ nodeIndex: { a: 0, b: 1 },
68
+ indexNode: ['a', 'b'],
69
+ capturedAt: 't',
70
+ };
71
+ expect(coherenceScore(m)).toBe(-Infinity);
72
+ });
73
+ });
74
+
75
+ describe('complexity class budget', () => {
76
+ it('logarithmic fits within linear', () => {
77
+ expect(fitsBudget('logarithmic', 'linear')).toBe(true);
78
+ });
79
+
80
+ it('linearithmic does NOT fit within linear', () => {
81
+ expect(fitsBudget('linearithmic', 'linear')).toBe(false);
82
+ });
83
+
84
+ it('polylogarithmic is edge-safe', () => {
85
+ expect(isEdgeSafe('polylogarithmic')).toBe(true);
86
+ });
87
+
88
+ it('linear is NOT edge-safe', () => {
89
+ expect(isEdgeSafe('linear')).toBe(false);
90
+ });
91
+
92
+ it('observedComplexity reports logarithmic when iterations ≤ log2(n)', () => {
93
+ const obs = observedComplexity(3, 100);
94
+ expect(['constant', 'logarithmic']).toContain(obs);
95
+ });
96
+
97
+ it('observedComplexity reports linear when iterations ≈ n', () => {
98
+ expect(observedComplexity(100, 100)).toBe('linear');
99
+ });
100
+ });
101
+
102
+ describe('singleEntryPageRank', () => {
103
+ it('returns a non-negative score for a registered node', () => {
104
+ const m = ddMatrix(10);
105
+ const query: PageRankQuery = {
106
+ graphId: m.graphId,
107
+ nodeId: 'n5',
108
+ alpha: 0.85,
109
+ epsilon: 1e-3,
110
+ seedNodes: [],
111
+ maxComplexityClass: 'linear',
112
+ coherenceThreshold: 0,
113
+ };
114
+ const { score, iterations } = singleEntryPageRank(m, query);
115
+ expect(score).toBeGreaterThanOrEqual(0);
116
+ expect(iterations).toBeGreaterThan(0);
117
+ });
118
+
119
+ it('returns 0 for an unknown node', () => {
120
+ const m = ddMatrix(10);
121
+ const { score } = singleEntryPageRank(m, {
122
+ graphId: m.graphId,
123
+ nodeId: 'absent',
124
+ alpha: 0.85,
125
+ epsilon: 1e-3,
126
+ seedNodes: [],
127
+ maxComplexityClass: 'linear',
128
+ coherenceThreshold: 0,
129
+ });
130
+ expect(score).toBe(0);
131
+ });
132
+
133
+ it('honours personalized seed nodes', () => {
134
+ const m = ddMatrix(10);
135
+ const seeded = singleEntryPageRank(m, {
136
+ graphId: m.graphId,
137
+ nodeId: 'n0',
138
+ alpha: 0.85,
139
+ epsilon: 1e-3,
140
+ seedNodes: ['n0'],
141
+ maxComplexityClass: 'linear',
142
+ coherenceThreshold: 0,
143
+ });
144
+ const unseeded = singleEntryPageRank(m, {
145
+ graphId: m.graphId,
146
+ nodeId: 'n0',
147
+ alpha: 0.85,
148
+ epsilon: 1e-3,
149
+ seedNodes: [],
150
+ maxComplexityClass: 'linear',
151
+ coherenceThreshold: 0,
152
+ });
153
+ expect(seeded.score).toBeGreaterThan(unseeded.score);
154
+ });
155
+ });
156
+
157
+ describe('conjugateGradient', () => {
158
+ it('solves A·x = b on a DD system to residual < 1e-6', () => {
159
+ const m = ddMatrix(8);
160
+ const b = Array.from({ length: 8 }, () => 1);
161
+ const { x, residualNorm, iterations } = conjugateGradient(m, b, { epsilon: 1e-8, maxIter: 50 });
162
+ expect(x).toHaveLength(8);
163
+ expect(residualNorm).toBeLessThan(1e-6);
164
+ expect(iterations).toBeLessThan(50);
165
+ });
166
+ });
167
+
168
+ describe('neumann', () => {
169
+ it('converges on a DD system', () => {
170
+ const m = ddMatrix(8);
171
+ const b = Array.from({ length: 8 }, () => 1);
172
+ const { residualNorm, iterations } = neumann(m, b, { epsilon: 1e-6, maxIter: 200 });
173
+ expect(iterations).toBeLessThan(200);
174
+ expect(residualNorm).toBeLessThan(1e-3);
175
+ });
176
+ });
177
+
178
+ describe('solveOnChange', () => {
179
+ it('produces x ≈ x_full when delta points at the same RHS', () => {
180
+ const m = ddMatrix(8);
181
+ const b = Array.from({ length: 8 }, () => 1);
182
+ const baseline = conjugateGradient(m, b, { epsilon: 1e-8 });
183
+
184
+ const prev = new Array<number>(8).fill(0);
185
+ const delta = { indices: Array.from({ length: 8 }, (_, i) => i), values: b };
186
+ const { x } = solveOnChange(m, prev, delta, { epsilon: 1e-8, algorithm: 'cg' });
187
+
188
+ for (let i = 0; i < 8; i++) {
189
+ expect(Math.abs(x[i]! - baseline.x[i]!)).toBeLessThan(1e-3);
190
+ }
191
+ });
192
+
193
+ it('handles sparse delta (only one node updated)', () => {
194
+ const m = ddMatrix(8);
195
+ const prev = new Array<number>(8).fill(0.1);
196
+ const delta = { indices: [3], values: [0.5] };
197
+ const { x, iterations } = solveOnChange(m, prev, delta, { epsilon: 1e-8, algorithm: 'cg' });
198
+ expect(iterations).toBeLessThan(50);
199
+ expect(x).toHaveLength(8);
200
+ });
201
+ });
202
+
203
+ describe('hashResult', () => {
204
+ it('is deterministic for the same inputs', () => {
205
+ const a = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: [], score: 0.123 });
206
+ const b = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: [], score: 0.123 });
207
+ expect(a).toBe(b);
208
+ });
209
+
210
+ it('differs when content differs', () => {
211
+ const a = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: [], score: 0.1 });
212
+ const b = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: [], score: 0.2 });
213
+ expect(a).not.toBe(b);
214
+ });
215
+
216
+ it('treats seedNodes order as canonical', () => {
217
+ const a = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: ['a', 'b'], score: 0.1 });
218
+ const b = hashResult({ graphId: 'g', nodeId: 'n', alpha: 0.85, epsilon: 1e-3, seedNodes: ['b', 'a'], score: 0.1 });
219
+ expect(a).toBe(b);
220
+ });
221
+ });
222
+
223
+ describe('runPageRank — error paths', () => {
224
+ it('throws coherence-rejected when threshold not met', () => {
225
+ const m = ddMatrix(8);
226
+ expect(() =>
227
+ runPageRank(m, {
228
+ graphId: m.graphId,
229
+ nodeId: 'n0',
230
+ alpha: 0.85,
231
+ epsilon: 1e-3,
232
+ seedNodes: [],
233
+ maxComplexityClass: 'linear',
234
+ coherenceThreshold: 0.99,
235
+ }),
236
+ ).toThrow();
237
+ });
238
+
239
+ it('returns a populated result on the happy path', () => {
240
+ const m = ddMatrix(8);
241
+ const result = runPageRank(m, {
242
+ graphId: m.graphId,
243
+ nodeId: 'n3',
244
+ alpha: 0.85,
245
+ epsilon: 1e-3,
246
+ seedNodes: [],
247
+ maxComplexityClass: 'linear',
248
+ coherenceThreshold: 0,
249
+ });
250
+ expect(result.score).toBeGreaterThanOrEqual(0);
251
+ expect(result.complexityClass).toBeDefined();
252
+ expect(result.coherence.passed).toBe(true);
253
+ expect(result.resultHash).toMatch(/^[0-9a-f]{64}$/);
254
+ });
255
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "tests"]
21
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ environment: 'node',
7
+ globals: false,
8
+ },
9
+ });