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,102 @@
1
+ /**
2
+ * AIDefence Suspicion Adapter (Wedge 10, ADR-123 Phase 6)
3
+ *
4
+ * `ruflo-aidefence` flags syscalls / agent actions as suspicious. This
5
+ * adapter exports the call-graph so suspicion propagates from the flagged
6
+ * leaf node back through callers via single-entry PR. `α=0.95` (high
7
+ * decay — suspicion travels far) by convention.
8
+ */
9
+
10
+ import { createHash } from 'node:crypto';
11
+ import type { SparseEntry, SparseMatrix } from '../domain/types.js';
12
+ import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
13
+ import { getRegistry } from '../domain/adapter.js';
14
+
15
+ export interface CallEdge {
16
+ callerId: string;
17
+ calleeId: string;
18
+ /** Optional weight — calls per session. Default 1. */
19
+ weight?: number;
20
+ }
21
+
22
+ export interface AIDefenceSource {
23
+ listCallEdges(): Promise<readonly CallEdge[]>;
24
+ }
25
+
26
+ export interface AIDefenceAdapterOptions {
27
+ source: AIDefenceSource;
28
+ /** DD safety margin. Default 0.25. */
29
+ ddSafetyMargin?: number;
30
+ }
31
+
32
+ export const AIDEFENCE_CALL_GRAPH_ID = 'ruflo-aidefence:call-graph';
33
+
34
+ export class AIDefenceSuspicionAdapter implements SublinearAdapter {
35
+ readonly graphId = AIDEFENCE_CALL_GRAPH_ID;
36
+ readonly ownerPlugin = 'ruflo-aidefence';
37
+ readonly requiresPreprocessing = false;
38
+
39
+ private readonly source: AIDefenceSource;
40
+ private readonly ddSafetyMargin: number;
41
+
42
+ constructor(options: AIDefenceAdapterOptions) {
43
+ this.source = options.source;
44
+ this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
45
+ }
46
+
47
+ async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
48
+ const edges = await this.source.listCallEdges();
49
+ const idSet = new Set<string>();
50
+ for (const e of edges) {
51
+ idSet.add(e.callerId);
52
+ idSet.add(e.calleeId);
53
+ }
54
+ if (options?.nodeFilter) {
55
+ for (const n of [...idSet]) if (!options.nodeFilter.has(n)) idSet.delete(n);
56
+ }
57
+ const nodes = [...idSet].sort();
58
+ const nodeIndex: Record<string, number> = {};
59
+ nodes.forEach((n, i) => (nodeIndex[n] = i));
60
+
61
+ // Suspicion flows from callee BACK to caller, so edges are reversed:
62
+ // a callee that's flagged should bump suspicion on its caller.
63
+ const entries: SparseEntry[] = [];
64
+ const rowSums = new Array<number>(nodes.length).fill(0);
65
+ for (const e of edges) {
66
+ // Reverse direction: from callee → caller
67
+ const r = nodeIndex[e.calleeId];
68
+ const c = nodeIndex[e.callerId];
69
+ if (r === undefined || c === undefined || r === c) continue;
70
+ const w = Math.max(0, e.weight ?? 1);
71
+ entries.push({ row: r, col: c, value: w });
72
+ rowSums[r] += w;
73
+ }
74
+ for (let i = 0; i < nodes.length; i++) {
75
+ entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
76
+ }
77
+ return {
78
+ graphId: this.graphId,
79
+ size: nodes.length,
80
+ entries,
81
+ nodeIndex,
82
+ indexNode: nodes,
83
+ capturedAt: new Date().toISOString(),
84
+ contentHash: hashContent(this.graphId, entries),
85
+ };
86
+ }
87
+ }
88
+
89
+ export function registerAIDefenceSuspicionAdapter(
90
+ options: AIDefenceAdapterOptions & { registry?: AdapterRegistry },
91
+ ): AIDefenceSuspicionAdapter {
92
+ const adapter = new AIDefenceSuspicionAdapter(options);
93
+ (options.registry ?? getRegistry()).register(adapter);
94
+ return adapter;
95
+ }
96
+
97
+ function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
98
+ const h = createHash('sha256');
99
+ h.update(graphId);
100
+ for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
101
+ return h.digest('hex');
102
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Browser Causal-Recovery Adapter (Wedge 1, ADR-123 Phase 2)
3
+ *
4
+ * Exports the ADR-122 Phase 2 selector-break events as a SparseMatrix so
5
+ * `sublinear/page-rank-entry` can score a single element-ref's causal
6
+ * brittleness in O(log N) instead of the current O(N) `breakCount / attempts`
7
+ * ratio.
8
+ *
9
+ * The matrix `M` is built per-origin: rows + columns are union of
10
+ * - all element-refs that have ever appeared
11
+ * - all selector strings that have ever been retried
12
+ * Off-diagonal entry M[i,j] = number of times row-event preceded column-event
13
+ * on the same DOM mutation lineage (weight ≥ 1). Diagonal entry M[i,i] = 1 + Σ
14
+ * |off-diagonals on row i|, so the matrix is strictly diagonally-dominant.
15
+ *
16
+ * The adapter is **dependency-injection-friendly**: callers pass a
17
+ * `BreakEventSource` (a structural type matching the slice of ADR-122's
18
+ * CausalRecoveryService surface we actually need) so this plugin does NOT
19
+ * hard-import @claude-flow/browser. Phase 2 of *this* plugin ships the
20
+ * adapter; the browser package only needs to call `registerBrowserCausalAdapter()`
21
+ * at its plugin-init time.
22
+ */
23
+
24
+ import { createHash } from 'node:crypto';
25
+ import type { SparseEntry, SparseMatrix } from '../domain/types.js';
26
+ import type { SublinearAdapter } from '../domain/adapter.js';
27
+
28
+ /**
29
+ * The slice of @claude-flow/browser's CausalRecoveryService surface we depend on.
30
+ * Defined structurally so we don't need a hard import.
31
+ */
32
+ export interface BreakEventSource {
33
+ /** All recorded break events for an origin, in chronological order. */
34
+ listBreaks(origin: string): Promise<readonly BreakEventLike[]>;
35
+ }
36
+
37
+ /** Minimal break-event shape we read. Compatible with ADR-122 Phase 2. */
38
+ export interface BreakEventLike {
39
+ id: string;
40
+ origin: string;
41
+ selector: string;
42
+ /** Optional last-known role+name fuzzy-match keys (Phase 2 records both). */
43
+ lastKnownRole?: string;
44
+ lastKnownName?: string;
45
+ /** When the break was first observed (ISO). */
46
+ timestamp: string;
47
+ }
48
+
49
+ export interface BrowserCausalAdapterOptions {
50
+ /** Origin (e.g. `https://example.com`) — one adapter instance per origin. */
51
+ origin: string;
52
+ /** Event source. */
53
+ source: BreakEventSource;
54
+ /** Adjacency-weighting half-life in milliseconds. Default 24h. */
55
+ halfLifeMs?: number;
56
+ /** Diagonal-dominance margin to keep DD even under heavy noise. Default 0.5. */
57
+ ddSafetyMargin?: number;
58
+ }
59
+
60
+ /**
61
+ * Identifier convention so consumers can address per-origin graphs:
62
+ * `browser:causal:<origin>` — e.g. `browser:causal:https://example.com`
63
+ */
64
+ export function browserCausalGraphId(origin: string): string {
65
+ return `browser:causal:${origin}`;
66
+ }
67
+
68
+ export class BrowserCausalAdapter implements SublinearAdapter {
69
+ readonly graphId: string;
70
+ readonly ownerPlugin = '@claude-flow/browser';
71
+
72
+ private readonly origin: string;
73
+ private readonly source: BreakEventSource;
74
+ private readonly halfLifeMs: number;
75
+ private readonly ddSafetyMargin: number;
76
+
77
+ constructor(options: BrowserCausalAdapterOptions) {
78
+ this.origin = options.origin;
79
+ this.source = options.source;
80
+ this.halfLifeMs = options.halfLifeMs ?? 24 * 60 * 60 * 1000;
81
+ this.ddSafetyMargin = options.ddSafetyMargin ?? 0.5;
82
+ this.graphId = browserCausalGraphId(this.origin);
83
+ }
84
+
85
+ async exportAsSparseMatrix(options?: {
86
+ since?: string;
87
+ nodeFilter?: ReadonlySet<string>;
88
+ }): Promise<SparseMatrix> {
89
+ const events = await this.source.listBreaks(this.origin);
90
+ const cutoff = options?.since ? Date.parse(options.since) : -Infinity;
91
+ const filtered = events.filter((e) => Date.parse(e.timestamp) >= cutoff);
92
+
93
+ // Node set: union of selectors + (role:name) fuzzy keys
94
+ const nodeIds = new Set<string>();
95
+ for (const e of filtered) {
96
+ nodeIds.add(e.selector);
97
+ if (e.lastKnownRole && e.lastKnownName) {
98
+ nodeIds.add(`role:${e.lastKnownRole}:${e.lastKnownName}`);
99
+ }
100
+ }
101
+ if (options?.nodeFilter) {
102
+ for (const n of [...nodeIds]) if (!options.nodeFilter.has(n)) nodeIds.delete(n);
103
+ }
104
+
105
+ const nodes = [...nodeIds].sort();
106
+ const nodeIndex: Record<string, number> = {};
107
+ nodes.forEach((n, i) => (nodeIndex[n] = i));
108
+
109
+ // Build adjacency: row = source event's selector, col = "next" event's selector
110
+ // within the same chronological neighbourhood. Time-decayed weight.
111
+ const weights = new Map<string, number>(); // key = "row:col"
112
+ const now = Date.now();
113
+ for (let i = 0; i < filtered.length - 1; i++) {
114
+ const a = filtered[i];
115
+ const b = filtered[i + 1];
116
+ const decay = Math.exp(-(now - Date.parse(a.timestamp)) / this.halfLifeMs);
117
+ const keys = pairKeys(a, b);
118
+ for (const k of keys) weights.set(k, (weights.get(k) ?? 0) + decay);
119
+ }
120
+
121
+ // Emit off-diagonal entries, then add a diagonal that guarantees DD.
122
+ const entries: SparseEntry[] = [];
123
+ const rowSums = new Array<number>(nodes.length).fill(0);
124
+ for (const [key, w] of weights) {
125
+ const [from, to] = key.split('::');
126
+ const rIdx = nodeIndex[from!];
127
+ const cIdx = nodeIndex[to!];
128
+ if (rIdx === undefined || cIdx === undefined) continue;
129
+ if (rIdx === cIdx) continue;
130
+ entries.push({ row: rIdx, col: cIdx, value: w });
131
+ rowSums[rIdx] += Math.abs(w);
132
+ }
133
+ for (let i = 0; i < nodes.length; i++) {
134
+ const diag = rowSums[i]! + this.ddSafetyMargin;
135
+ entries.push({ row: i, col: i, value: Math.max(1, diag) });
136
+ }
137
+
138
+ return {
139
+ graphId: this.graphId,
140
+ size: nodes.length,
141
+ entries,
142
+ nodeIndex,
143
+ indexNode: nodes,
144
+ capturedAt: new Date().toISOString(),
145
+ contentHash: hashContent(this.graphId, entries),
146
+ };
147
+ }
148
+
149
+ readonly requiresPreprocessing = false;
150
+ }
151
+
152
+ function pairKeys(a: BreakEventLike, b: BreakEventLike): string[] {
153
+ const keys: string[] = [`${a.selector}::${b.selector}`];
154
+ if (a.lastKnownRole && a.lastKnownName) {
155
+ const key = `role:${a.lastKnownRole}:${a.lastKnownName}`;
156
+ keys.push(`${key}::${b.selector}`);
157
+ }
158
+ if (b.lastKnownRole && b.lastKnownName) {
159
+ const key = `role:${b.lastKnownRole}:${b.lastKnownName}`;
160
+ keys.push(`${a.selector}::${key}`);
161
+ }
162
+ return keys;
163
+ }
164
+
165
+ function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
166
+ const h = createHash('sha256');
167
+ h.update(graphId);
168
+ for (const e of entries) {
169
+ h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
170
+ }
171
+ return h.digest('hex');
172
+ }
173
+
174
+ /**
175
+ * Convenience entry point — instantiate + register in one call.
176
+ *
177
+ * Intended to be invoked from @claude-flow/browser's init code:
178
+ * import { registerBrowserCausalAdapter } from 'ruflo-graph-intelligence/adapters';
179
+ * registerBrowserCausalAdapter({ origin: 'https://example.com', source: causalService });
180
+ */
181
+ export function registerBrowserCausalAdapter(
182
+ options: BrowserCausalAdapterOptions & { registry?: import('../domain/adapter.js').AdapterRegistry },
183
+ ): BrowserCausalAdapter {
184
+ const adapter = new BrowserCausalAdapter(options);
185
+ const registry = options.registry ?? import('../domain/adapter.js').then((m) => m.getRegistry());
186
+ // Synchronous registration when registry is supplied; lazy fallback otherwise.
187
+ if (typeof (registry as { register?: unknown }).register === 'function') {
188
+ (registry as import('../domain/adapter.js').AdapterRegistry).register(adapter);
189
+ } else {
190
+ (registry as Promise<import('../domain/adapter.js').AdapterRegistry>).then((r) => r.register(adapter));
191
+ }
192
+ return adapter;
193
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Cost Attribution Adapter (Wedge 6, ADR-123 Phase 3)
3
+ *
4
+ * `ruflo-cost-tracker` records token usage per session in a causation graph:
5
+ * user-prompt → spawned-agent → MCP-call → model-invocation → tokens-USD
6
+ *
7
+ * This adapter exports that graph so `sublinear/page-rank-entry` answers
8
+ * "which root prompt caused the most downstream spend" in O(log traces)
9
+ * rather than O(traces) walks. Costs are one-way edges → asymmetric matrix.
10
+ */
11
+
12
+ import { createHash } from 'node:crypto';
13
+ import type { SparseEntry, SparseMatrix } from '../domain/types.js';
14
+ import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
15
+ import { getRegistry } from '../domain/adapter.js';
16
+
17
+ export interface CostCausationEdge {
18
+ /** Parent node — typically a prompt id or upstream agent id. */
19
+ parentId: string;
20
+ /** Child node — typically a spawned agent / MCP call / model invocation id. */
21
+ childId: string;
22
+ /** USD cost attributed to the child as caused by this parent. */
23
+ costUsd: number;
24
+ }
25
+
26
+ export interface CostCausationSource {
27
+ /** All causation edges for a session (or globally). */
28
+ listCausationEdges(sessionId?: string): Promise<readonly CostCausationEdge[]>;
29
+ }
30
+
31
+ export interface CostAttributionAdapterOptions {
32
+ source: CostCausationSource;
33
+ /** Restrict to a specific session id. */
34
+ sessionId?: string;
35
+ /** DD safety margin. Default 0.25. */
36
+ ddSafetyMargin?: number;
37
+ }
38
+
39
+ export function costAttributionGraphId(sessionId?: string): string {
40
+ return sessionId ? `ruflo-cost-tracker:causation:${sessionId}` : 'ruflo-cost-tracker:causation:global';
41
+ }
42
+
43
+ export class CostAttributionAdapter implements SublinearAdapter {
44
+ readonly graphId: string;
45
+ readonly ownerPlugin = 'ruflo-cost-tracker';
46
+ readonly requiresPreprocessing = false;
47
+
48
+ private readonly source: CostCausationSource;
49
+ private readonly sessionId?: string;
50
+ private readonly ddSafetyMargin: number;
51
+
52
+ constructor(options: CostAttributionAdapterOptions) {
53
+ this.source = options.source;
54
+ this.sessionId = options.sessionId;
55
+ this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
56
+ this.graphId = costAttributionGraphId(this.sessionId);
57
+ }
58
+
59
+ async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
60
+ const edges = await this.source.listCausationEdges(this.sessionId);
61
+ const nodeSet = new Set<string>();
62
+ for (const e of edges) {
63
+ nodeSet.add(e.parentId);
64
+ nodeSet.add(e.childId);
65
+ }
66
+ if (options?.nodeFilter) {
67
+ for (const n of [...nodeSet]) if (!options.nodeFilter.has(n)) nodeSet.delete(n);
68
+ }
69
+
70
+ const nodes = [...nodeSet].sort();
71
+ const nodeIndex: Record<string, number> = {};
72
+ nodes.forEach((n, i) => (nodeIndex[n] = i));
73
+
74
+ // Normalise costs into [0, 1] per row so PageRank semantics make sense
75
+ // (we're after the *share of blame*, not the raw dollar amount).
76
+ const rawRowSums = new Array<number>(nodes.length).fill(0);
77
+ for (const e of edges) {
78
+ const r = nodeIndex[e.parentId];
79
+ if (r === undefined) continue;
80
+ rawRowSums[r]! += Math.max(0, e.costUsd);
81
+ }
82
+ const entries: SparseEntry[] = [];
83
+ const rowSums = new Array<number>(nodes.length).fill(0);
84
+ for (const e of edges) {
85
+ const r = nodeIndex[e.parentId];
86
+ const c = nodeIndex[e.childId];
87
+ if (r === undefined || c === undefined || r === c) continue;
88
+ const denom = rawRowSums[r]!;
89
+ if (denom === 0) continue;
90
+ const w = Math.max(0, e.costUsd) / denom;
91
+ if (w === 0) continue;
92
+ entries.push({ row: r, col: c, value: w });
93
+ rowSums[r] += w;
94
+ }
95
+ for (let i = 0; i < nodes.length; i++) {
96
+ entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
97
+ }
98
+ return {
99
+ graphId: this.graphId,
100
+ size: nodes.length,
101
+ entries,
102
+ nodeIndex,
103
+ indexNode: nodes,
104
+ capturedAt: new Date().toISOString(),
105
+ contentHash: hashContent(this.graphId, entries),
106
+ };
107
+ }
108
+ }
109
+
110
+ export function registerCostAttributionAdapter(
111
+ options: CostAttributionAdapterOptions & { registry?: AdapterRegistry },
112
+ ): CostAttributionAdapter {
113
+ const adapter = new CostAttributionAdapter(options);
114
+ (options.registry ?? getRegistry()).register(adapter);
115
+ return adapter;
116
+ }
117
+
118
+ function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
119
+ const h = createHash('sha256');
120
+ h.update(graphId);
121
+ for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
122
+ return h.digest('hex');
123
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Federation Trust Adapter (Wedge 3, ADR-123 Phase 3)
3
+ *
4
+ * `ruflo-federation` ships a peer trust mesh (ADR-097/104/105/111). This
5
+ * adapter exports the mesh as a SparseMatrix so `sublinear/page-rank-entry`
6
+ * computes transitive trust `(I − αT)τ = e` in O(log peers) instead of
7
+ * O(peers²) closure walks. Trust is one-way → the matrix is asymmetric
8
+ * (per upstream 2025 asymmetric-DD result, this is in-scope for sublinear).
9
+ */
10
+
11
+ import { createHash } from 'node:crypto';
12
+ import type { SparseEntry, SparseMatrix } from '../domain/types.js';
13
+ import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
14
+ import { getRegistry } from '../domain/adapter.js';
15
+
16
+ export interface PeerTrustEdge {
17
+ /** Peer the trust comes from. */
18
+ fromPeer: string;
19
+ /** Peer the trust is directed at. */
20
+ toPeer: string;
21
+ /** Confidence in [0, 1] — typically derived from signed-message hit rate. */
22
+ confidence: number;
23
+ /** When the edge was last updated. */
24
+ updatedAt: string;
25
+ }
26
+
27
+ export interface PeerTrustSource {
28
+ /** All trust edges currently in the local view of the mesh. */
29
+ listTrustEdges(): Promise<readonly PeerTrustEdge[]>;
30
+ }
31
+
32
+ export interface FederationTrustAdapterOptions {
33
+ source: PeerTrustSource;
34
+ /** Edge-staleness cutoff in milliseconds. Default 7 days. */
35
+ freshnessMs?: number;
36
+ /** Diagonal-dominance safety margin. Default 0.25. */
37
+ ddSafetyMargin?: number;
38
+ }
39
+
40
+ export const FEDERATION_TRUST_GRAPH_ID = 'ruflo-federation:trust-mesh';
41
+
42
+ export class FederationTrustAdapter implements SublinearAdapter {
43
+ readonly graphId = FEDERATION_TRUST_GRAPH_ID;
44
+ readonly ownerPlugin = 'ruflo-federation';
45
+ readonly requiresPreprocessing = false;
46
+
47
+ private readonly source: PeerTrustSource;
48
+ private readonly freshnessMs: number;
49
+ private readonly ddSafetyMargin: number;
50
+
51
+ constructor(options: FederationTrustAdapterOptions) {
52
+ this.source = options.source;
53
+ this.freshnessMs = options.freshnessMs ?? 7 * 24 * 60 * 60 * 1000;
54
+ this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
55
+ }
56
+
57
+ async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
58
+ const allEdges = await this.source.listTrustEdges();
59
+ const now = Date.now();
60
+ const fresh = allEdges.filter((e) => now - Date.parse(e.updatedAt) <= this.freshnessMs);
61
+
62
+ const peerSet = new Set<string>();
63
+ for (const e of fresh) {
64
+ peerSet.add(e.fromPeer);
65
+ peerSet.add(e.toPeer);
66
+ }
67
+ if (options?.nodeFilter) {
68
+ for (const p of [...peerSet]) if (!options.nodeFilter.has(p)) peerSet.delete(p);
69
+ }
70
+
71
+ const peers = [...peerSet].sort();
72
+ const nodeIndex: Record<string, number> = {};
73
+ peers.forEach((p, i) => (nodeIndex[p] = i));
74
+
75
+ const entries: SparseEntry[] = [];
76
+ const rowSums = new Array<number>(peers.length).fill(0);
77
+ for (const edge of fresh) {
78
+ const r = nodeIndex[edge.fromPeer];
79
+ const c = nodeIndex[edge.toPeer];
80
+ if (r === undefined || c === undefined || r === c) continue;
81
+ const w = Math.max(0, Math.min(1, edge.confidence));
82
+ if (w === 0) continue;
83
+ entries.push({ row: r, col: c, value: w });
84
+ rowSums[r] += w;
85
+ }
86
+ // DD diagonal: |diag| ≥ Σ|off| + margin
87
+ for (let i = 0; i < peers.length; i++) {
88
+ entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
89
+ }
90
+
91
+ return {
92
+ graphId: this.graphId,
93
+ size: peers.length,
94
+ entries,
95
+ nodeIndex,
96
+ indexNode: peers,
97
+ capturedAt: new Date().toISOString(),
98
+ contentHash: hashContent(this.graphId, entries),
99
+ };
100
+ }
101
+ }
102
+
103
+ export function registerFederationTrustAdapter(
104
+ options: FederationTrustAdapterOptions & { registry?: AdapterRegistry },
105
+ ): FederationTrustAdapter {
106
+ const adapter = new FederationTrustAdapter(options);
107
+ (options.registry ?? getRegistry()).register(adapter);
108
+ return adapter;
109
+ }
110
+
111
+ function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
112
+ const h = createHash('sha256');
113
+ h.update(graphId);
114
+ for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
115
+ return h.digest('hex');
116
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * ruflo-graph-intelligence — Adapter barrel export
3
+ *
4
+ * Each adapter lives in its own file and is opted into by the owning plugin
5
+ * at init time. Phase-1 ships zero adapters; Phase 2+ adds them one at a time.
6
+ */
7
+
8
+ export {
9
+ BrowserCausalAdapter,
10
+ browserCausalGraphId,
11
+ registerBrowserCausalAdapter,
12
+ type BreakEventSource,
13
+ type BreakEventLike,
14
+ type BrowserCausalAdapterOptions,
15
+ } from './browser-causal-adapter.js';
16
+
17
+ export {
18
+ FederationTrustAdapter,
19
+ FEDERATION_TRUST_GRAPH_ID,
20
+ registerFederationTrustAdapter,
21
+ type PeerTrustEdge,
22
+ type PeerTrustSource,
23
+ type FederationTrustAdapterOptions,
24
+ } from './federation-trust-adapter.js';
25
+
26
+ export {
27
+ CostAttributionAdapter,
28
+ costAttributionGraphId,
29
+ registerCostAttributionAdapter,
30
+ type CostCausationEdge,
31
+ type CostCausationSource,
32
+ type CostAttributionAdapterOptions,
33
+ } from './cost-attribution-adapter.js';
34
+
35
+ export {
36
+ ObservabilitySpanAdapter,
37
+ observabilityGraphId,
38
+ registerObservabilitySpanAdapter,
39
+ type SpanRecord,
40
+ type ObservabilitySpanSource,
41
+ type ObservabilitySpanAdapterOptions,
42
+ } from './observability-span-adapter.js';
43
+
44
+ export {
45
+ KnowledgeGraphAdapter,
46
+ KNOWLEDGE_GRAPH_ID,
47
+ registerKnowledgeGraphAdapter,
48
+ type KGEdge,
49
+ type KnowledgeGraphSource,
50
+ type KnowledgeGraphAdapterOptions,
51
+ } from './knowledge-graph-adapter.js';
52
+
53
+ export {
54
+ RagMemoryAdapter,
55
+ ragMemoryGraphId,
56
+ registerRagMemoryAdapter,
57
+ type ChunkEdge,
58
+ type RagMemorySource,
59
+ type RagMemoryAdapterOptions,
60
+ } from './rag-memory-adapter.js';
61
+
62
+ export {
63
+ PortfolioCovarianceAdapter,
64
+ portfolioGraphId,
65
+ registerPortfolioCovarianceAdapter,
66
+ type CovarianceEntry,
67
+ type PortfolioSource,
68
+ type PortfolioAdapterOptions,
69
+ } from './portfolio-cg-adapter.js';
70
+
71
+ export {
72
+ AIDefenceSuspicionAdapter,
73
+ AIDEFENCE_CALL_GRAPH_ID,
74
+ registerAIDefenceSuspicionAdapter,
75
+ type CallEdge,
76
+ type AIDefenceSource,
77
+ type AIDefenceAdapterOptions,
78
+ } from './aidefence-suspicion-adapter.js';
79
+
80
+ export {
81
+ JujutsuBlastRadiusAdapter,
82
+ JUJUTSU_IMPORT_GRAPH_ID,
83
+ registerJujutsuBlastRadiusAdapter,
84
+ type ImportEdge,
85
+ type JujutsuSource,
86
+ type JujutsuAdapterOptions,
87
+ } from './jujutsu-blast-radius-adapter.js';