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,147 @@
1
+ /**
2
+ * Federation Client for PR Artifacts (Phase 8 — beyond-SOTA, ADR-123)
3
+ *
4
+ * Sends `pr_artifact_request` to peers, verifies inbound `pr_artifact_response`
5
+ * envelopes via Phase 7 verifier with the consumer's trust list, falls back
6
+ * to local recompute on stale/untrusted responses.
7
+ */
8
+
9
+ import { verifyArtifact } from '../infrastructure/witness-signer.js';
10
+ import { runPageRank } from '../infrastructure/solver-bridge.js';
11
+ import { getRegistry } from '../domain/adapter.js';
12
+ import type {
13
+ FederationMessage,
14
+ FederationTransport,
15
+ PrArtifactRequest,
16
+ PrArtifactResponse,
17
+ PrArtifactStale,
18
+ } from '../domain/federation-protocol.js';
19
+ import type { ArtifactVerificationResult } from '../domain/signed-artifact.js';
20
+ import type { PageRankResult } from '../domain/types.js';
21
+
22
+ export interface FederationClientOptions {
23
+ installationId: string;
24
+ transport: FederationTransport;
25
+ /** Public keys we trust. Empty = trust any valid signature. */
26
+ trustedPublicKeys?: string[];
27
+ }
28
+
29
+ export interface FetchPrResult {
30
+ /** How the result was obtained. */
31
+ origin: 'peer' | 'local-fallback' | 'stale-fallback' | 'untrusted-fallback';
32
+ result: PageRankResult;
33
+ /** If origin === 'peer', the verification report. */
34
+ verification?: ArtifactVerificationResult;
35
+ /** Optional reason when we fell back. */
36
+ fallbackReason?: string;
37
+ }
38
+
39
+ export class FederationClient {
40
+ private readonly installationId: string;
41
+ private readonly transport: FederationTransport;
42
+ private readonly trustedPublicKeys: string[];
43
+
44
+ constructor(options: FederationClientOptions) {
45
+ this.installationId = options.installationId;
46
+ this.transport = options.transport;
47
+ this.trustedPublicKeys = options.trustedPublicKeys ?? [];
48
+ }
49
+
50
+ /**
51
+ * Fetch a single-entry PR score for `nodeId` over `graphId`. Tries the
52
+ * peer first; on stale/untrusted/missing, falls back to local recompute.
53
+ */
54
+ async fetchPageRank(input: {
55
+ peer: string;
56
+ graphId: string;
57
+ nodeId: string;
58
+ alpha?: number;
59
+ epsilon?: number;
60
+ seedNodes?: string[];
61
+ lastKnownGraphHash?: string;
62
+ lastKnownResultHash?: string;
63
+ }): Promise<FetchPrResult> {
64
+ const request: PrArtifactRequest = {
65
+ type: 'pr_artifact_request',
66
+ fromInstallation: this.installationId,
67
+ graphId: input.graphId,
68
+ queryNode: input.nodeId,
69
+ alpha: input.alpha ?? 0.85,
70
+ epsilon: input.epsilon ?? 1e-3,
71
+ seedNodes: input.seedNodes ?? [],
72
+ lastKnownGraphHash: input.lastKnownGraphHash,
73
+ lastKnownResultHash: input.lastKnownResultHash,
74
+ };
75
+ const response = await this.transport.send(input.peer, request);
76
+ if (response && response.type === 'pr_artifact_response') {
77
+ const verification = verifyArtifact(response.envelope, {
78
+ trustedPublicKeys: this.trustedPublicKeys.length > 0 ? this.trustedPublicKeys : undefined,
79
+ });
80
+ if (verification.valid) {
81
+ return {
82
+ origin: 'peer',
83
+ result: response.envelope.payload.result,
84
+ verification,
85
+ };
86
+ }
87
+ return {
88
+ origin: 'untrusted-fallback',
89
+ result: await this.localCompute(input),
90
+ verification,
91
+ fallbackReason: verification.reason ?? 'verification failed',
92
+ };
93
+ }
94
+ if (response && response.type === 'pr_artifact_stale') {
95
+ return {
96
+ origin: 'stale-fallback',
97
+ result: await this.localCompute(input),
98
+ fallbackReason: response.reason,
99
+ };
100
+ }
101
+ // No usable response
102
+ return {
103
+ origin: 'local-fallback',
104
+ result: await this.localCompute(input),
105
+ fallbackReason: 'no usable response from peer',
106
+ };
107
+ }
108
+
109
+ private async localCompute(input: {
110
+ graphId: string;
111
+ nodeId: string;
112
+ alpha?: number;
113
+ epsilon?: number;
114
+ seedNodes?: string[];
115
+ }): Promise<PageRankResult> {
116
+ const adapter = getRegistry().get(input.graphId);
117
+ if (!adapter) {
118
+ throw new Error(`localCompute: no adapter for graphId=${input.graphId}`);
119
+ }
120
+ const matrix = await adapter.exportAsSparseMatrix();
121
+ return runPageRank(matrix, {
122
+ graphId: input.graphId,
123
+ nodeId: input.nodeId,
124
+ alpha: input.alpha ?? 0.85,
125
+ epsilon: input.epsilon ?? 1e-3,
126
+ seedNodes: input.seedNodes ?? [],
127
+ maxComplexityClass: 'polynomial',
128
+ coherenceThreshold: 0,
129
+ });
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Helper: an in-process transport stitching a client to a server. Useful for
135
+ * testing the Phase 8 round-trip without spinning up real ADR-104 wiring.
136
+ */
137
+ export function inProcessTransport(server: { handle: (msg: FederationMessage) => Promise<FederationMessage | null> }): FederationTransport {
138
+ return {
139
+ async send(_to, msg) {
140
+ return server.handle(msg);
141
+ },
142
+ onMessage() {
143
+ // not used in the in-process variant
144
+ return () => {};
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Federation Server for PR Artifacts (Phase 8 — beyond-SOTA, ADR-123)
3
+ *
4
+ * Receives `pr_artifact_request` messages and serves either a fresh signed
5
+ * artifact (if the holder has one cached or computes one on demand) or a
6
+ * stale-rejection. The server uses Phase 7's sealArtifact for signing and
7
+ * Phase 2-6 adapters for graph access.
8
+ *
9
+ * Pluggable via FederationTransport so ADR-104's real wire layer fits in
10
+ * unchanged.
11
+ */
12
+
13
+ import { sealArtifact, type WitnessKey } from '../infrastructure/witness-signer.js';
14
+ import { runPageRank } from '../infrastructure/solver-bridge.js';
15
+ import { getRegistry } from '../domain/adapter.js';
16
+ import type {
17
+ FederationMessage,
18
+ FederationTransport,
19
+ PrArtifactRequest,
20
+ PrArtifactResponse,
21
+ PrArtifactStale,
22
+ } from '../domain/federation-protocol.js';
23
+
24
+ export interface FederationServerOptions {
25
+ installationId: string;
26
+ witnessKey: WitnessKey;
27
+ witnessKeyId: string;
28
+ transport: FederationTransport;
29
+ /** Per-graph rate-limit budget — requests per peer per minute. */
30
+ rateLimitPerPeerPerMinute?: number;
31
+ }
32
+
33
+ interface RateBucket {
34
+ count: number;
35
+ resetAt: number;
36
+ }
37
+
38
+ export class FederationServer {
39
+ private readonly installationId: string;
40
+ private readonly witnessKey: WitnessKey;
41
+ private readonly witnessKeyId: string;
42
+ private readonly transport: FederationTransport;
43
+ private readonly rateLimit: number;
44
+ private rateBuckets = new Map<string, RateBucket>();
45
+ private unsubscribe?: () => void;
46
+
47
+ constructor(options: FederationServerOptions) {
48
+ this.installationId = options.installationId;
49
+ this.witnessKey = options.witnessKey;
50
+ this.witnessKeyId = options.witnessKeyId;
51
+ this.transport = options.transport;
52
+ this.rateLimit = options.rateLimitPerPeerPerMinute ?? 60;
53
+ }
54
+
55
+ start(): void {
56
+ this.unsubscribe = this.transport.onMessage((m) => this.handle(m));
57
+ }
58
+
59
+ stop(): void {
60
+ this.unsubscribe?.();
61
+ }
62
+
63
+ /** Public test seam — handles a single inbound message. */
64
+ async handle(msg: FederationMessage): Promise<FederationMessage | null> {
65
+ if (msg.type !== 'pr_artifact_request') return null;
66
+ return this.handleRequest(msg);
67
+ }
68
+
69
+ private async handleRequest(req: PrArtifactRequest): Promise<FederationMessage> {
70
+ // Rate-limit per peer
71
+ if (!this.checkRate(req.fromInstallation)) {
72
+ return {
73
+ type: 'pr_artifact_stale',
74
+ fromInstallation: this.installationId,
75
+ graphId: req.graphId,
76
+ currentGraphHash: 'rate-limited'.padEnd(64, '0'),
77
+ reason: 'rate limit exceeded',
78
+ } satisfies PrArtifactStale;
79
+ }
80
+
81
+ const adapter = getRegistry().get(req.graphId);
82
+ if (!adapter) {
83
+ return {
84
+ type: 'pr_artifact_stale',
85
+ fromInstallation: this.installationId,
86
+ graphId: req.graphId,
87
+ currentGraphHash: '0'.repeat(64),
88
+ reason: `no adapter for graphId=${req.graphId}`,
89
+ } satisfies PrArtifactStale;
90
+ }
91
+
92
+ const matrix = await adapter.exportAsSparseMatrix();
93
+ const currentGraphHash = matrix.contentHash ?? '0'.repeat(64);
94
+
95
+ // Stale-check: if the requester's lastKnownGraphHash differs, send stale
96
+ if (req.lastKnownGraphHash && req.lastKnownGraphHash !== currentGraphHash) {
97
+ return {
98
+ type: 'pr_artifact_stale',
99
+ fromInstallation: this.installationId,
100
+ graphId: req.graphId,
101
+ currentGraphHash,
102
+ reason: 'graph has changed since requester last cached',
103
+ } satisfies PrArtifactStale;
104
+ }
105
+
106
+ if (!req.queryNode) {
107
+ return {
108
+ type: 'pr_artifact_stale',
109
+ fromInstallation: this.installationId,
110
+ graphId: req.graphId,
111
+ currentGraphHash,
112
+ reason: 'full-vector requests not yet supported (Phase 8 only)',
113
+ } satisfies PrArtifactStale;
114
+ }
115
+
116
+ // Compute and seal
117
+ const prResult = runPageRank(matrix, {
118
+ graphId: req.graphId,
119
+ nodeId: req.queryNode,
120
+ alpha: req.alpha,
121
+ epsilon: req.epsilon,
122
+ seedNodes: req.seedNodes,
123
+ maxComplexityClass: 'polynomial',
124
+ coherenceThreshold: 0,
125
+ });
126
+ const { envelope } = sealArtifact({
127
+ installationId: this.installationId,
128
+ witnessKeyId: this.witnessKeyId,
129
+ graphId: req.graphId,
130
+ graphHash: currentGraphHash,
131
+ graphTimestamp: matrix.capturedAt,
132
+ algorithm: 'forward-push',
133
+ alpha: req.alpha,
134
+ epsilon: req.epsilon,
135
+ queryNode: req.queryNode,
136
+ seedNodes: req.seedNodes,
137
+ result: prResult,
138
+ witnessKey: this.witnessKey,
139
+ });
140
+ return {
141
+ type: 'pr_artifact_response',
142
+ fromInstallation: this.installationId,
143
+ envelope,
144
+ } satisfies PrArtifactResponse;
145
+ }
146
+
147
+ private checkRate(peer: string): boolean {
148
+ const now = Date.now();
149
+ const bucket = this.rateBuckets.get(peer);
150
+ if (!bucket || now >= bucket.resetAt) {
151
+ this.rateBuckets.set(peer, { count: 1, resetAt: now + 60_000 });
152
+ return true;
153
+ }
154
+ if (bucket.count >= this.rateLimit) return false;
155
+ bucket.count++;
156
+ return true;
157
+ }
158
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Streaming Bridge — Wedge 12 (ADR-123 Phase 6.5)
3
+ *
4
+ * Couples a registered SublinearAdapter with `solve_on_change` so event-driven
5
+ * graphs (federation trust deltas, span streams, append-only causal breaks,
6
+ * cost spend events, AIDefence flag updates) pay only `O(nnz(delta) · log N)`
7
+ * per event rather than recomputing the full vector each tick.
8
+ *
9
+ * The bridge maintains the *previous solution* in memory; every push call
10
+ * applies the delta and returns the updated solution. It also exposes a
11
+ * `crossoverHeuristic()` that decides whether the cheap delta path is
12
+ * actually cheaper than a full re-solve given the current density.
13
+ */
14
+
15
+ import type { SublinearAdapter } from '../domain/adapter.js';
16
+ import type { ComplexityClass, SparseDelta, SparseMatrix } from '../domain/types.js';
17
+ import { runSolveOnChange, runSolve } from '../infrastructure/solver-bridge.js';
18
+
19
+ export interface StreamingBridgeOptions {
20
+ adapter: SublinearAdapter;
21
+ /** Initial b vector for the base full-solve. */
22
+ initialRhs: number[];
23
+ algorithm?: 'cg' | 'neumann';
24
+ maxComplexityClass?: ComplexityClass;
25
+ /**
26
+ * Crossover threshold: prefer `solve_on_change` when
27
+ * `nnz(delta) / nnz(matrix) < deltaRatioThreshold`. Default 0.05.
28
+ */
29
+ deltaRatioThreshold?: number;
30
+ /** Force full re-solve after N delta updates regardless. Default 50. */
31
+ refreshEvery?: number;
32
+ }
33
+
34
+ export interface StreamingUpdate {
35
+ x: number[];
36
+ residualNorm: number;
37
+ iterations: number;
38
+ /** How this update was computed — informational. */
39
+ mode: 'delta' | 'full-resolve' | 'cold-start';
40
+ deltaNnz?: number;
41
+ appliedAt: string;
42
+ }
43
+
44
+ export class StreamingBridge {
45
+ private readonly adapter: SublinearAdapter;
46
+ private readonly initialRhs: number[];
47
+ private readonly algorithm: 'cg' | 'neumann';
48
+ private readonly maxComplexityClass: ComplexityClass;
49
+ private readonly deltaRatioThreshold: number;
50
+ private readonly refreshEvery: number;
51
+
52
+ private prevSolution: number[] | undefined;
53
+ private deltaCount = 0;
54
+ private cachedMatrix: SparseMatrix | undefined;
55
+
56
+ constructor(options: StreamingBridgeOptions) {
57
+ this.adapter = options.adapter;
58
+ this.initialRhs = options.initialRhs;
59
+ this.algorithm = options.algorithm ?? 'cg';
60
+ this.maxComplexityClass = options.maxComplexityClass ?? 'polynomial';
61
+ this.deltaRatioThreshold = options.deltaRatioThreshold ?? 0.05;
62
+ this.refreshEvery = options.refreshEvery ?? 50;
63
+ }
64
+
65
+ /** Force a fresh full re-solve and reset the streaming state. */
66
+ async coldStart(): Promise<StreamingUpdate> {
67
+ const matrix = await this.adapter.exportAsSparseMatrix();
68
+ this.cachedMatrix = matrix;
69
+ const result = runSolve(matrix, {
70
+ graphId: matrix.graphId,
71
+ rhs: this.initialRhs,
72
+ algorithm: this.algorithm,
73
+ maxComplexityClass: this.maxComplexityClass,
74
+ coherenceThreshold: 0,
75
+ });
76
+ this.prevSolution = result.x;
77
+ this.deltaCount = 0;
78
+ return {
79
+ x: result.x,
80
+ residualNorm: result.residualNorm,
81
+ iterations: result.iterations,
82
+ mode: 'cold-start',
83
+ appliedAt: new Date().toISOString(),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Apply a delta event. The bridge picks `solve_on_change` if the delta is
89
+ * sparse enough; otherwise it falls back to a full re-solve.
90
+ */
91
+ async pushDelta(delta: SparseDelta): Promise<StreamingUpdate> {
92
+ if (!this.prevSolution || !this.cachedMatrix) {
93
+ await this.coldStart();
94
+ }
95
+ // Refresh-cap forces a clean re-solve to bound drift error
96
+ if (this.deltaCount >= this.refreshEvery) {
97
+ return this.coldStart().then((u) => ({ ...u, mode: 'full-resolve' as const }));
98
+ }
99
+
100
+ const matrix = this.cachedMatrix!;
101
+ const deltaRatio = delta.indices.length / Math.max(1, matrix.entries.length);
102
+ if (deltaRatio >= this.deltaRatioThreshold) {
103
+ // Too dense — full re-solve is cheaper than delta-and-correct
104
+ return this.coldStart().then((u) => ({ ...u, mode: 'full-resolve' as const }));
105
+ }
106
+
107
+ const result = runSolveOnChange(matrix, {
108
+ graphId: matrix.graphId,
109
+ prevSolution: this.prevSolution!,
110
+ delta,
111
+ algorithm: this.algorithm,
112
+ maxComplexityClass: this.maxComplexityClass,
113
+ });
114
+ this.prevSolution = result.x;
115
+ this.deltaCount++;
116
+ return {
117
+ x: result.x,
118
+ residualNorm: result.residualNorm,
119
+ iterations: result.iterations,
120
+ mode: 'delta',
121
+ deltaNnz: delta.indices.length,
122
+ appliedAt: new Date().toISOString(),
123
+ };
124
+ }
125
+
126
+ /** Best-effort current solution snapshot. */
127
+ getCurrentSolution(): readonly number[] | undefined {
128
+ return this.prevSolution;
129
+ }
130
+
131
+ /** Reset cached state (e.g. after the underlying graph re-grew). */
132
+ reset(): void {
133
+ this.prevSolution = undefined;
134
+ this.cachedMatrix = undefined;
135
+ this.deltaCount = 0;
136
+ }
137
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ruflo-graph-intelligence — Adapter Contract (ADR-123 § Architecture)
3
+ *
4
+ * Each owning plugin (browser, federation, knowledge-graph, …) implements
5
+ * this interface and registers itself at plugin-load time. The registry is
6
+ * plugin-local — the source plugin understands its own storage layout best.
7
+ */
8
+
9
+ import type { SparseMatrix } from './types.js';
10
+
11
+ export interface SublinearAdapter {
12
+ /** Stable identifier — `"ruflo-federation:trust-mesh"`, etc. */
13
+ readonly graphId: string;
14
+
15
+ /** Owning plugin's package name; informational. */
16
+ readonly ownerPlugin: string;
17
+
18
+ /**
19
+ * Export the current graph state as a SparseMatrix.
20
+ *
21
+ * `since` is an optional snapshot ISO timestamp — adapters may return
22
+ * a frozen snapshot at that moment (for reproducibility) instead of
23
+ * the live state. Adapters that don't support history return the live
24
+ * state regardless.
25
+ *
26
+ * `nodeFilter` is an optional allow-list — adapters that can prune
27
+ * irrelevant rows should honour it to keep the matrix small. Adapters
28
+ * that can't filter return the full matrix.
29
+ */
30
+ exportAsSparseMatrix(options?: {
31
+ since?: string;
32
+ nodeFilter?: ReadonlySet<string>;
33
+ }): Promise<SparseMatrix>;
34
+
35
+ /**
36
+ * Best-effort streaming hook — adapters that can emit deltas (new
37
+ * causal events, new federation trust updates, new spans) push
38
+ * `SparseDelta`s through this listener. Adapters that can't return
39
+ * `noopUnsubscribe`.
40
+ */
41
+ onChange?(listener: (delta: import('./types.js').SparseDelta) => void): () => void;
42
+
43
+ /**
44
+ * Whether this graph is **structurally** non-DD (e.g. asymmetric trust with
45
+ * negative weights). Adapters that know they need clamping/renormalisation
46
+ * before submission set this flag so the registry can advise callers.
47
+ */
48
+ readonly requiresPreprocessing?: boolean;
49
+ }
50
+
51
+ /** No-op unsubscribe for adapters that don't support streaming. */
52
+ export const noopUnsubscribe = (): void => {};
53
+
54
+ /** Plugin-load-time registry — populated via `register()`. */
55
+ export class AdapterRegistry {
56
+ private adapters = new Map<string, SublinearAdapter>();
57
+
58
+ register(adapter: SublinearAdapter): void {
59
+ if (this.adapters.has(adapter.graphId)) {
60
+ throw new Error(`adapter ${adapter.graphId} already registered`);
61
+ }
62
+ this.adapters.set(adapter.graphId, adapter);
63
+ }
64
+
65
+ unregister(graphId: string): boolean {
66
+ return this.adapters.delete(graphId);
67
+ }
68
+
69
+ get(graphId: string): SublinearAdapter | undefined {
70
+ return this.adapters.get(graphId);
71
+ }
72
+
73
+ list(): SublinearAdapter[] {
74
+ return [...this.adapters.values()];
75
+ }
76
+
77
+ clear(): void {
78
+ this.adapters.clear();
79
+ }
80
+ }
81
+
82
+ /** Singleton registry — same lifetime as the plugin process. */
83
+ let globalRegistry: AdapterRegistry | undefined;
84
+ export function getRegistry(): AdapterRegistry {
85
+ if (!globalRegistry) globalRegistry = new AdapterRegistry();
86
+ return globalRegistry;
87
+ }
88
+
89
+ /** Test hook. */
90
+ export function resetRegistry(): void {
91
+ globalRegistry = undefined;
92
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Federation Protocol for PR Artifacts (Phase 8 — beyond-SOTA, ADR-123)
3
+ *
4
+ * Wire message types over the ADR-104 federation transport. Peers can ask
5
+ * for a precomputed signed PageRank vector instead of re-walking the graph,
6
+ * verify via the Phase 7 signer, and fall back to local recompute when the
7
+ * artifact is stale (graphHash mismatch) or the holder isn't trusted.
8
+ */
9
+
10
+ import { z } from 'zod';
11
+ import { SignedPageRankEnvelopeSchema } from './signed-artifact.js';
12
+
13
+ // ============================================================================
14
+ // Request types
15
+ // ============================================================================
16
+
17
+ export const PrArtifactRequestSchema = z.object({
18
+ type: z.literal('pr_artifact_request'),
19
+ /** Requesting installation id. */
20
+ fromInstallation: z.string().min(1),
21
+ /** Adapter graph id to query. */
22
+ graphId: z.string().min(1),
23
+ /** Query node for single-entry PR. Omit for full-vector requests. */
24
+ queryNode: z.string().optional(),
25
+ alpha: z.number().positive().lt(1).default(0.85),
26
+ epsilon: z.number().positive().default(1e-3),
27
+ seedNodes: z.array(z.string()).default([]),
28
+ /** Hash of the graph the requester has locally. Holder uses this to decide
29
+ * whether to ship a fresh artifact or a delta. */
30
+ lastKnownGraphHash: z.string().regex(/^[0-9a-f]{64}$/).optional(),
31
+ /** Hash of any previous PR result the requester has cached. */
32
+ lastKnownResultHash: z.string().regex(/^[0-9a-f]{64}$/).optional(),
33
+ });
34
+ export type PrArtifactRequest = z.infer<typeof PrArtifactRequestSchema>;
35
+
36
+ // ============================================================================
37
+ // Response types — either a full artifact, a delta, or a stale-rejection
38
+ // ============================================================================
39
+
40
+ export const PrArtifactResponseSchema = z.object({
41
+ type: z.literal('pr_artifact_response'),
42
+ /** Responding installation id. */
43
+ fromInstallation: z.string().min(1),
44
+ /** The Phase 7 signed envelope. */
45
+ envelope: SignedPageRankEnvelopeSchema,
46
+ });
47
+ export type PrArtifactResponse = z.infer<typeof PrArtifactResponseSchema>;
48
+
49
+ /** Lightweight delta — only the score difference + new resultHash. */
50
+ export const PrArtifactDeltaSchema = z.object({
51
+ type: z.literal('pr_artifact_delta'),
52
+ fromInstallation: z.string().min(1),
53
+ graphId: z.string().min(1),
54
+ /** Hash the delta is based on. */
55
+ baseResultHash: z.string().regex(/^[0-9a-f]{64}$/),
56
+ /** New resultHash after delta applied. */
57
+ newResultHash: z.string().regex(/^[0-9a-f]{64}$/),
58
+ /** Score delta (single-entry) or sparse delta indices+values (vector). */
59
+ scoreDelta: z.number().optional(),
60
+ sparseDelta: z.object({
61
+ indices: z.array(z.number().int().nonnegative()),
62
+ values: z.array(z.number()),
63
+ }).optional(),
64
+ });
65
+ export type PrArtifactDelta = z.infer<typeof PrArtifactDeltaSchema>;
66
+
67
+ export const PrArtifactStaleSchema = z.object({
68
+ type: z.literal('pr_artifact_stale'),
69
+ fromInstallation: z.string().min(1),
70
+ graphId: z.string().min(1),
71
+ /** The holder's current graph hash so the requester can decide what to do. */
72
+ currentGraphHash: z.string().regex(/^[0-9a-f]{64}$/),
73
+ /** Why the request was stale-rejected. */
74
+ reason: z.string(),
75
+ });
76
+ export type PrArtifactStale = z.infer<typeof PrArtifactStaleSchema>;
77
+
78
+ export const FederationMessageSchema = z.discriminatedUnion('type', [
79
+ PrArtifactRequestSchema,
80
+ PrArtifactResponseSchema,
81
+ PrArtifactDeltaSchema,
82
+ PrArtifactStaleSchema,
83
+ ]);
84
+ export type FederationMessage = z.infer<typeof FederationMessageSchema>;
85
+
86
+ // ============================================================================
87
+ // Transport contract — ADR-104 plugs in here
88
+ // ============================================================================
89
+
90
+ export interface FederationTransport {
91
+ /** Send a message to a specific peer. Returns the response message. */
92
+ send(toInstallation: string, message: FederationMessage): Promise<FederationMessage | null>;
93
+ /** Subscribe to inbound messages. */
94
+ onMessage(handler: (msg: FederationMessage) => Promise<FederationMessage | null>): () => void;
95
+ }