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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jujutsu Blast-Radius Adapter (Wedge 11, ADR-123 Phase 6)
|
|
3
|
+
*
|
|
4
|
+
* `ruflo-jujutsu` runs diff-analyze. This adapter exports the file-import
|
|
5
|
+
* graph so "if I change `foo.ts`, which files are downstream-affected" is a
|
|
6
|
+
* single-entry PR query — O(log files) instead of O(LOC × imports).
|
|
7
|
+
*
|
|
8
|
+
* The graph is directed: A imports B → A is downstream of B (B's change
|
|
9
|
+
* propagates upward to A). For blast-radius FROM a changed file, the matrix
|
|
10
|
+
* keeps natural orientation (changed file is the seed; downstream files
|
|
11
|
+
* receive the PR mass).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import type { SparseEntry, SparseMatrix } from '../domain/types.js';
|
|
16
|
+
import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
|
|
17
|
+
import { getRegistry } from '../domain/adapter.js';
|
|
18
|
+
|
|
19
|
+
export interface ImportEdge {
|
|
20
|
+
/** File that does the importing. */
|
|
21
|
+
importer: string;
|
|
22
|
+
/** File being imported. */
|
|
23
|
+
importee: string;
|
|
24
|
+
/** Optional weight — number of distinct symbols imported. Default 1. */
|
|
25
|
+
weight?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JujutsuSource {
|
|
29
|
+
listImportEdges(): Promise<readonly ImportEdge[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface JujutsuAdapterOptions {
|
|
33
|
+
source: JujutsuSource;
|
|
34
|
+
/** DD safety margin. Default 0.25. */
|
|
35
|
+
ddSafetyMargin?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const JUJUTSU_IMPORT_GRAPH_ID = 'ruflo-jujutsu:import-graph';
|
|
39
|
+
|
|
40
|
+
export class JujutsuBlastRadiusAdapter implements SublinearAdapter {
|
|
41
|
+
readonly graphId = JUJUTSU_IMPORT_GRAPH_ID;
|
|
42
|
+
readonly ownerPlugin = 'ruflo-jujutsu';
|
|
43
|
+
readonly requiresPreprocessing = false;
|
|
44
|
+
|
|
45
|
+
private readonly source: JujutsuSource;
|
|
46
|
+
private readonly ddSafetyMargin: number;
|
|
47
|
+
|
|
48
|
+
constructor(options: JujutsuAdapterOptions) {
|
|
49
|
+
this.source = options.source;
|
|
50
|
+
this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
|
|
54
|
+
const edges = await this.source.listImportEdges();
|
|
55
|
+
const fileSet = new Set<string>();
|
|
56
|
+
for (const e of edges) {
|
|
57
|
+
fileSet.add(e.importer);
|
|
58
|
+
fileSet.add(e.importee);
|
|
59
|
+
}
|
|
60
|
+
if (options?.nodeFilter) {
|
|
61
|
+
for (const n of [...fileSet]) if (!options.nodeFilter.has(n)) fileSet.delete(n);
|
|
62
|
+
}
|
|
63
|
+
const files = [...fileSet].sort();
|
|
64
|
+
const nodeIndex: Record<string, number> = {};
|
|
65
|
+
files.forEach((f, i) => (nodeIndex[f] = i));
|
|
66
|
+
|
|
67
|
+
// For blast-radius PR seeded at the changed file: we want change to flow
|
|
68
|
+
// from `importee` → `importer`. So row = importee, col = importer.
|
|
69
|
+
const entries: SparseEntry[] = [];
|
|
70
|
+
const rowSums = new Array<number>(files.length).fill(0);
|
|
71
|
+
for (const e of edges) {
|
|
72
|
+
const r = nodeIndex[e.importee];
|
|
73
|
+
const c = nodeIndex[e.importer];
|
|
74
|
+
if (r === undefined || c === undefined || r === c) continue;
|
|
75
|
+
const w = Math.max(0, e.weight ?? 1);
|
|
76
|
+
entries.push({ row: r, col: c, value: w });
|
|
77
|
+
rowSums[r] += w;
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < files.length; i++) {
|
|
80
|
+
entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
graphId: this.graphId,
|
|
84
|
+
size: files.length,
|
|
85
|
+
entries,
|
|
86
|
+
nodeIndex,
|
|
87
|
+
indexNode: files,
|
|
88
|
+
capturedAt: new Date().toISOString(),
|
|
89
|
+
contentHash: hashContent(this.graphId, entries),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerJujutsuBlastRadiusAdapter(
|
|
95
|
+
options: JujutsuAdapterOptions & { registry?: AdapterRegistry },
|
|
96
|
+
): JujutsuBlastRadiusAdapter {
|
|
97
|
+
const adapter = new JujutsuBlastRadiusAdapter(options);
|
|
98
|
+
(options.registry ?? getRegistry()).register(adapter);
|
|
99
|
+
return adapter;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
|
|
103
|
+
const h = createHash('sha256');
|
|
104
|
+
h.update(graphId);
|
|
105
|
+
for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
|
|
106
|
+
return h.digest('hex');
|
|
107
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Adapter (Wedge 4, ADR-123 Phase 4)
|
|
3
|
+
*
|
|
4
|
+
* `ruflo-knowledge-graph` builds an entity-relation graph via kg-extract.
|
|
5
|
+
* This adapter exports it as a SparseMatrix so kg-importance(entity) becomes
|
|
6
|
+
* a single-entry PR query — answering "which entity is most central" in
|
|
7
|
+
* sub-millisecond on a 10k-node graph.
|
|
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 KGEdge {
|
|
16
|
+
fromEntity: string;
|
|
17
|
+
toEntity: string;
|
|
18
|
+
relation: string;
|
|
19
|
+
/** Edge confidence in [0,1]. Default 1.0. */
|
|
20
|
+
confidence?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KnowledgeGraphSource {
|
|
24
|
+
listEdges(): Promise<readonly KGEdge[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface KnowledgeGraphAdapterOptions {
|
|
28
|
+
source: KnowledgeGraphSource;
|
|
29
|
+
/** DD safety margin. Default 0.25. */
|
|
30
|
+
ddSafetyMargin?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const KNOWLEDGE_GRAPH_ID = 'ruflo-knowledge-graph:entities';
|
|
34
|
+
|
|
35
|
+
export class KnowledgeGraphAdapter implements SublinearAdapter {
|
|
36
|
+
readonly graphId = KNOWLEDGE_GRAPH_ID;
|
|
37
|
+
readonly ownerPlugin = 'ruflo-knowledge-graph';
|
|
38
|
+
readonly requiresPreprocessing = false;
|
|
39
|
+
|
|
40
|
+
private readonly source: KnowledgeGraphSource;
|
|
41
|
+
private readonly ddSafetyMargin: number;
|
|
42
|
+
|
|
43
|
+
constructor(options: KnowledgeGraphAdapterOptions) {
|
|
44
|
+
this.source = options.source;
|
|
45
|
+
this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
|
|
49
|
+
const edges = await this.source.listEdges();
|
|
50
|
+
const entitySet = new Set<string>();
|
|
51
|
+
for (const e of edges) {
|
|
52
|
+
entitySet.add(e.fromEntity);
|
|
53
|
+
entitySet.add(e.toEntity);
|
|
54
|
+
}
|
|
55
|
+
if (options?.nodeFilter) {
|
|
56
|
+
for (const n of [...entitySet]) if (!options.nodeFilter.has(n)) entitySet.delete(n);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entities = [...entitySet].sort();
|
|
60
|
+
const nodeIndex: Record<string, number> = {};
|
|
61
|
+
entities.forEach((n, i) => (nodeIndex[n] = i));
|
|
62
|
+
|
|
63
|
+
// Weight edges by confidence; if multiple relations exist between two
|
|
64
|
+
// entities, sum confidences (cap at 1).
|
|
65
|
+
const weights = new Map<string, number>();
|
|
66
|
+
for (const e of edges) {
|
|
67
|
+
const r = nodeIndex[e.fromEntity];
|
|
68
|
+
const c = nodeIndex[e.toEntity];
|
|
69
|
+
if (r === undefined || c === undefined || r === c) continue;
|
|
70
|
+
const key = `${r},${c}`;
|
|
71
|
+
weights.set(key, Math.min(1, (weights.get(key) ?? 0) + (e.confidence ?? 1)));
|
|
72
|
+
}
|
|
73
|
+
const entries: SparseEntry[] = [];
|
|
74
|
+
const rowSums = new Array<number>(entities.length).fill(0);
|
|
75
|
+
for (const [key, w] of weights) {
|
|
76
|
+
const [rStr, cStr] = key.split(',');
|
|
77
|
+
const r = Number(rStr);
|
|
78
|
+
const c = Number(cStr);
|
|
79
|
+
entries.push({ row: r, col: c, value: w });
|
|
80
|
+
rowSums[r] += w;
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < entities.length; i++) {
|
|
83
|
+
entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
graphId: this.graphId,
|
|
87
|
+
size: entities.length,
|
|
88
|
+
entries,
|
|
89
|
+
nodeIndex,
|
|
90
|
+
indexNode: entities,
|
|
91
|
+
capturedAt: new Date().toISOString(),
|
|
92
|
+
contentHash: hashContent(this.graphId, entries),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerKnowledgeGraphAdapter(
|
|
98
|
+
options: KnowledgeGraphAdapterOptions & { registry?: AdapterRegistry },
|
|
99
|
+
): KnowledgeGraphAdapter {
|
|
100
|
+
const adapter = new KnowledgeGraphAdapter(options);
|
|
101
|
+
(options.registry ?? getRegistry()).register(adapter);
|
|
102
|
+
return adapter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
|
|
106
|
+
const h = createHash('sha256');
|
|
107
|
+
h.update(graphId);
|
|
108
|
+
for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
|
|
109
|
+
return h.digest('hex');
|
|
110
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Span Adapter (Wedge 7, ADR-123 Phase 3)
|
|
3
|
+
*
|
|
4
|
+
* `ruflo-observability` records spans (OpenTelemetry-style) with parent links
|
|
5
|
+
* + cross-trace causality. This adapter exports the span graph so the user
|
|
6
|
+
* can ask "which span most contributed to this slow/failed request" in
|
|
7
|
+
* O(log spans) instead of full-trace walks.
|
|
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 SpanRecord {
|
|
16
|
+
spanId: string;
|
|
17
|
+
parentSpanId?: string;
|
|
18
|
+
traceId: string;
|
|
19
|
+
/** Self-time in microseconds — used as edge weight. */
|
|
20
|
+
selfTimeUs: number;
|
|
21
|
+
/** Optional cross-trace causality link (e.g. an event triggered this trace). */
|
|
22
|
+
causedBySpanId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ObservabilitySpanSource {
|
|
26
|
+
listSpans(traceId: string): Promise<readonly SpanRecord[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ObservabilitySpanAdapterOptions {
|
|
30
|
+
source: ObservabilitySpanSource;
|
|
31
|
+
traceId: string;
|
|
32
|
+
/** DD safety margin. Default 0.25. */
|
|
33
|
+
ddSafetyMargin?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function observabilityGraphId(traceId: string): string {
|
|
37
|
+
return `ruflo-observability:trace:${traceId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ObservabilitySpanAdapter implements SublinearAdapter {
|
|
41
|
+
readonly graphId: string;
|
|
42
|
+
readonly ownerPlugin = 'ruflo-observability';
|
|
43
|
+
readonly requiresPreprocessing = false;
|
|
44
|
+
|
|
45
|
+
private readonly source: ObservabilitySpanSource;
|
|
46
|
+
private readonly traceId: string;
|
|
47
|
+
private readonly ddSafetyMargin: number;
|
|
48
|
+
|
|
49
|
+
constructor(options: ObservabilitySpanAdapterOptions) {
|
|
50
|
+
this.source = options.source;
|
|
51
|
+
this.traceId = options.traceId;
|
|
52
|
+
this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
|
|
53
|
+
this.graphId = observabilityGraphId(this.traceId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
|
|
57
|
+
const spans = await this.source.listSpans(this.traceId);
|
|
58
|
+
const idSet = new Set<string>();
|
|
59
|
+
for (const s of spans) {
|
|
60
|
+
idSet.add(s.spanId);
|
|
61
|
+
if (s.parentSpanId) idSet.add(s.parentSpanId);
|
|
62
|
+
if (s.causedBySpanId) idSet.add(s.causedBySpanId);
|
|
63
|
+
}
|
|
64
|
+
if (options?.nodeFilter) {
|
|
65
|
+
for (const n of [...idSet]) if (!options.nodeFilter.has(n)) idSet.delete(n);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nodes = [...idSet].sort();
|
|
69
|
+
const nodeIndex: Record<string, number> = {};
|
|
70
|
+
nodes.forEach((n, i) => (nodeIndex[n] = i));
|
|
71
|
+
|
|
72
|
+
// Normalise self-time per parent: each parent's outbound weights sum to ≤1.
|
|
73
|
+
const childrenByParent = new Map<string, SpanRecord[]>();
|
|
74
|
+
for (const s of spans) {
|
|
75
|
+
const p = s.parentSpanId ?? s.causedBySpanId;
|
|
76
|
+
if (!p) continue;
|
|
77
|
+
if (!childrenByParent.has(p)) childrenByParent.set(p, []);
|
|
78
|
+
childrenByParent.get(p)!.push(s);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const entries: SparseEntry[] = [];
|
|
82
|
+
const rowSums = new Array<number>(nodes.length).fill(0);
|
|
83
|
+
for (const [parent, children] of childrenByParent) {
|
|
84
|
+
const r = nodeIndex[parent];
|
|
85
|
+
if (r === undefined) continue;
|
|
86
|
+
const total = children.reduce((s, c) => s + Math.max(1, c.selfTimeUs), 0);
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
const c = nodeIndex[child.spanId];
|
|
89
|
+
if (c === undefined || c === r) continue;
|
|
90
|
+
const w = Math.max(1, child.selfTimeUs) / total;
|
|
91
|
+
entries.push({ row: r, col: c, value: w });
|
|
92
|
+
rowSums[r] += w;
|
|
93
|
+
}
|
|
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 registerObservabilitySpanAdapter(
|
|
111
|
+
options: ObservabilitySpanAdapterOptions & { registry?: AdapterRegistry },
|
|
112
|
+
): ObservabilitySpanAdapter {
|
|
113
|
+
const adapter = new ObservabilitySpanAdapter(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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio Covariance Adapter (Wedge 8, ADR-123 Phase 5)
|
|
3
|
+
*
|
|
4
|
+
* `ruflo-neural-trader` does mean-variance optimisation: solve `Σ x = μ`
|
|
5
|
+
* where `Σ` is the symmetric positive-definite asset-covariance matrix and
|
|
6
|
+
* `μ` is the vector of expected returns. CG is the ideal target — upstream
|
|
7
|
+
* 1.7.0 benchmarks: **816 ns at n=256, 40-60× faster than Neumann**.
|
|
8
|
+
*
|
|
9
|
+
* This adapter exports the covariance matrix as a SparseMatrix so callers
|
|
10
|
+
* use `sublinear/solve` with `algorithm: 'cg'` and get the optimal weights.
|
|
11
|
+
* Unlike the PageRank wedges, this graph IS symmetric (Σ = Σᵀ) and SPD by
|
|
12
|
+
* construction.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import type { SparseEntry, SparseMatrix } from '../domain/types.js';
|
|
17
|
+
import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
|
|
18
|
+
import { getRegistry } from '../domain/adapter.js';
|
|
19
|
+
|
|
20
|
+
export interface CovarianceEntry {
|
|
21
|
+
/** Asset symbol on the row side. */
|
|
22
|
+
assetA: string;
|
|
23
|
+
/** Asset symbol on the column side. */
|
|
24
|
+
assetB: string;
|
|
25
|
+
/** Covariance value. Σ[A,B] = Σ[B,A] by SPD construction. */
|
|
26
|
+
covariance: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PortfolioSource {
|
|
30
|
+
/**
|
|
31
|
+
* Dense or sparse covariance entries. Adapter symmetrises automatically
|
|
32
|
+
* if only one of (A,B) or (B,A) is provided.
|
|
33
|
+
*/
|
|
34
|
+
listCovarianceEntries(portfolioId: string): Promise<readonly CovarianceEntry[]>;
|
|
35
|
+
/** Expected returns vector — same keys as covariance assets. */
|
|
36
|
+
listExpectedReturns(portfolioId: string): Promise<Record<string, number>>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PortfolioAdapterOptions {
|
|
40
|
+
source: PortfolioSource;
|
|
41
|
+
portfolioId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Ridge regularisation added to the diagonal to ensure SPD even when the
|
|
44
|
+
* empirical covariance is rank-deficient. Default 1e-6.
|
|
45
|
+
*/
|
|
46
|
+
ridge?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function portfolioGraphId(portfolioId: string): string {
|
|
50
|
+
return `ruflo-neural-trader:covariance:${portfolioId}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class PortfolioCovarianceAdapter implements SublinearAdapter {
|
|
54
|
+
readonly graphId: string;
|
|
55
|
+
readonly ownerPlugin = 'ruflo-neural-trader';
|
|
56
|
+
readonly requiresPreprocessing = false;
|
|
57
|
+
|
|
58
|
+
private readonly source: PortfolioSource;
|
|
59
|
+
private readonly portfolioId: string;
|
|
60
|
+
private readonly ridge: number;
|
|
61
|
+
|
|
62
|
+
constructor(options: PortfolioAdapterOptions) {
|
|
63
|
+
this.source = options.source;
|
|
64
|
+
this.portfolioId = options.portfolioId;
|
|
65
|
+
this.ridge = options.ridge ?? 1e-6;
|
|
66
|
+
this.graphId = portfolioGraphId(this.portfolioId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
|
|
70
|
+
const covEntries = await this.source.listCovarianceEntries(this.portfolioId);
|
|
71
|
+
const assetSet = new Set<string>();
|
|
72
|
+
for (const e of covEntries) {
|
|
73
|
+
assetSet.add(e.assetA);
|
|
74
|
+
assetSet.add(e.assetB);
|
|
75
|
+
}
|
|
76
|
+
if (options?.nodeFilter) {
|
|
77
|
+
for (const a of [...assetSet]) if (!options.nodeFilter.has(a)) assetSet.delete(a);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const assets = [...assetSet].sort();
|
|
81
|
+
const nodeIndex: Record<string, number> = {};
|
|
82
|
+
assets.forEach((a, i) => (nodeIndex[a] = i));
|
|
83
|
+
|
|
84
|
+
// Symmetrise. If only Σ[A,B] is provided, also emit Σ[B,A] with the same value.
|
|
85
|
+
const sym = new Map<string, number>();
|
|
86
|
+
for (const e of covEntries) {
|
|
87
|
+
const r = nodeIndex[e.assetA];
|
|
88
|
+
const c = nodeIndex[e.assetB];
|
|
89
|
+
if (r === undefined || c === undefined) continue;
|
|
90
|
+
const k1 = `${r},${c}`;
|
|
91
|
+
const k2 = `${c},${r}`;
|
|
92
|
+
sym.set(k1, e.covariance);
|
|
93
|
+
sym.set(k2, e.covariance);
|
|
94
|
+
}
|
|
95
|
+
const entries: SparseEntry[] = [];
|
|
96
|
+
for (const [key, v] of sym) {
|
|
97
|
+
const [rStr, cStr] = key.split(',');
|
|
98
|
+
const r = Number(rStr);
|
|
99
|
+
const c = Number(cStr);
|
|
100
|
+
if (r === c) continue;
|
|
101
|
+
entries.push({ row: r, col: c, value: v });
|
|
102
|
+
}
|
|
103
|
+
// Diagonal: existing variance value (if provided) PLUS ridge — keeps SPD
|
|
104
|
+
// even when the empirical Σ is rank-deficient.
|
|
105
|
+
for (let i = 0; i < assets.length; i++) {
|
|
106
|
+
const provided = sym.get(`${i},${i}`) ?? 0;
|
|
107
|
+
entries.push({ row: i, col: i, value: provided + this.ridge });
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
graphId: this.graphId,
|
|
111
|
+
size: assets.length,
|
|
112
|
+
entries,
|
|
113
|
+
nodeIndex,
|
|
114
|
+
indexNode: assets,
|
|
115
|
+
capturedAt: new Date().toISOString(),
|
|
116
|
+
contentHash: hashContent(this.graphId, entries),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Fetch expected returns aligned with the matrix's node order. */
|
|
121
|
+
async expectedReturnsVector(matrix: SparseMatrix): Promise<number[]> {
|
|
122
|
+
const expected = await this.source.listExpectedReturns(this.portfolioId);
|
|
123
|
+
return matrix.indexNode.map((a) => expected[a] ?? 0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function registerPortfolioCovarianceAdapter(
|
|
128
|
+
options: PortfolioAdapterOptions & { registry?: AdapterRegistry },
|
|
129
|
+
): PortfolioCovarianceAdapter {
|
|
130
|
+
const adapter = new PortfolioCovarianceAdapter(options);
|
|
131
|
+
(options.registry ?? getRegistry()).register(adapter);
|
|
132
|
+
return adapter;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
|
|
136
|
+
const h = createHash('sha256');
|
|
137
|
+
h.update(graphId);
|
|
138
|
+
for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
|
|
139
|
+
return h.digest('hex');
|
|
140
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG Memory Adapter (Wedge 5, ADR-123 Phase 4)
|
|
3
|
+
*
|
|
4
|
+
* `ruflo-rag-memory` ships Graph-RAG multi-hop retrieval. This adapter
|
|
5
|
+
* exports the chunk-connectivity graph so personalized PR seeded by the
|
|
6
|
+
* query embedding ranks candidate chunks globally — graph-aware retrieval
|
|
7
|
+
* beyond flat top-k MMR rerank.
|
|
8
|
+
*
|
|
9
|
+
* The seedNodes for personalized PR come from the caller's query side
|
|
10
|
+
* (`sublinear/page-rank-entry` already accepts seedNodes). This adapter
|
|
11
|
+
* just exposes the underlying chunk graph.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import type { SparseEntry, SparseMatrix } from '../domain/types.js';
|
|
16
|
+
import type { SublinearAdapter, AdapterRegistry } from '../domain/adapter.js';
|
|
17
|
+
import { getRegistry } from '../domain/adapter.js';
|
|
18
|
+
|
|
19
|
+
export interface ChunkEdge {
|
|
20
|
+
fromChunkId: string;
|
|
21
|
+
toChunkId: string;
|
|
22
|
+
/** Similarity weight in [0, 1] — typically cosine similarity of chunk embeddings. */
|
|
23
|
+
similarity: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RagMemorySource {
|
|
27
|
+
listChunkEdges(namespace?: string): Promise<readonly ChunkEdge[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RagMemoryAdapterOptions {
|
|
31
|
+
source: RagMemorySource;
|
|
32
|
+
namespace?: string;
|
|
33
|
+
/** Minimum similarity to include an edge. Default 0.5. */
|
|
34
|
+
similarityFloor?: number;
|
|
35
|
+
/** DD safety margin. Default 0.25. */
|
|
36
|
+
ddSafetyMargin?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ragMemoryGraphId(namespace?: string): string {
|
|
40
|
+
return namespace
|
|
41
|
+
? `ruflo-rag-memory:chunks:${namespace}`
|
|
42
|
+
: 'ruflo-rag-memory:chunks:default';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class RagMemoryAdapter implements SublinearAdapter {
|
|
46
|
+
readonly graphId: string;
|
|
47
|
+
readonly ownerPlugin = 'ruflo-rag-memory';
|
|
48
|
+
readonly requiresPreprocessing = false;
|
|
49
|
+
|
|
50
|
+
private readonly source: RagMemorySource;
|
|
51
|
+
private readonly namespace?: string;
|
|
52
|
+
private readonly similarityFloor: number;
|
|
53
|
+
private readonly ddSafetyMargin: number;
|
|
54
|
+
|
|
55
|
+
constructor(options: RagMemoryAdapterOptions) {
|
|
56
|
+
this.source = options.source;
|
|
57
|
+
this.namespace = options.namespace;
|
|
58
|
+
this.similarityFloor = options.similarityFloor ?? 0.5;
|
|
59
|
+
this.ddSafetyMargin = options.ddSafetyMargin ?? 0.25;
|
|
60
|
+
this.graphId = ragMemoryGraphId(this.namespace);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async exportAsSparseMatrix(options?: { nodeFilter?: ReadonlySet<string> }): Promise<SparseMatrix> {
|
|
64
|
+
const edges = (await this.source.listChunkEdges(this.namespace)).filter(
|
|
65
|
+
(e) => e.similarity >= this.similarityFloor,
|
|
66
|
+
);
|
|
67
|
+
const chunkSet = new Set<string>();
|
|
68
|
+
for (const e of edges) {
|
|
69
|
+
chunkSet.add(e.fromChunkId);
|
|
70
|
+
chunkSet.add(e.toChunkId);
|
|
71
|
+
}
|
|
72
|
+
if (options?.nodeFilter) {
|
|
73
|
+
for (const n of [...chunkSet]) if (!options.nodeFilter.has(n)) chunkSet.delete(n);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const chunks = [...chunkSet].sort();
|
|
77
|
+
const nodeIndex: Record<string, number> = {};
|
|
78
|
+
chunks.forEach((n, i) => (nodeIndex[n] = i));
|
|
79
|
+
|
|
80
|
+
const entries: SparseEntry[] = [];
|
|
81
|
+
const rowSums = new Array<number>(chunks.length).fill(0);
|
|
82
|
+
for (const e of edges) {
|
|
83
|
+
const r = nodeIndex[e.fromChunkId];
|
|
84
|
+
const c = nodeIndex[e.toChunkId];
|
|
85
|
+
if (r === undefined || c === undefined || r === c) continue;
|
|
86
|
+
entries.push({ row: r, col: c, value: e.similarity });
|
|
87
|
+
rowSums[r] += e.similarity;
|
|
88
|
+
}
|
|
89
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
90
|
+
entries.push({ row: i, col: i, value: rowSums[i]! + this.ddSafetyMargin });
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
graphId: this.graphId,
|
|
94
|
+
size: chunks.length,
|
|
95
|
+
entries,
|
|
96
|
+
nodeIndex,
|
|
97
|
+
indexNode: chunks,
|
|
98
|
+
capturedAt: new Date().toISOString(),
|
|
99
|
+
contentHash: hashContent(this.graphId, entries),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function registerRagMemoryAdapter(
|
|
105
|
+
options: RagMemoryAdapterOptions & { registry?: AdapterRegistry },
|
|
106
|
+
): RagMemoryAdapter {
|
|
107
|
+
const adapter = new RagMemoryAdapter(options);
|
|
108
|
+
(options.registry ?? getRegistry()).register(adapter);
|
|
109
|
+
return adapter;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hashContent(graphId: string, entries: readonly SparseEntry[]): string {
|
|
113
|
+
const h = createHash('sha256');
|
|
114
|
+
h.update(graphId);
|
|
115
|
+
for (const e of entries) h.update(`|${e.row},${e.col},${e.value.toFixed(8)}`);
|
|
116
|
+
return h.digest('hex');
|
|
117
|
+
}
|