chainlesschain 0.51.0 → 0.81.0
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/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/a2a.js +380 -0
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/bi.js +348 -0
- package/src/commands/crosschain.js +218 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/dlp.js +341 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/evomap.js +394 -0
- package/src/commands/federation.js +283 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/inference.js +318 -0
- package/src/commands/lowcode.js +356 -0
- package/src/commands/marketplace.js +256 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/privacy.js +321 -0
- package/src/commands/reputation.js +261 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/siem.js +246 -0
- package/src/commands/sla.js +259 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/stress.js +230 -0
- package/src/commands/terraform.js +245 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +562 -1
- package/src/index.js +21 -0
- package/src/lib/a2a-protocol.js +451 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/app-builder.js +239 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/bi-engine.js +338 -0
- package/src/lib/cross-chain.js +345 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/dlp-engine.js +389 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/evomap-federation.js +177 -0
- package/src/lib/evomap-governance.js +276 -0
- package/src/lib/federation-hardening.js +259 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/inference-network.js +330 -0
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/privacy-computing.js +427 -0
- package/src/lib/reputation-optimizer.js +299 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/siem-exporter.js +333 -0
- package/src/lib/skill-marketplace.js +325 -0
- package/src/lib/sla-manager.js +275 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/stress-tester.js +330 -0
- package/src/lib/terraform-manager.js +363 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +523 -20
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Graph Analytics — centrality, community detection, shortest path,
|
|
3
|
+
* and composite influence scoring on snapshots produced by `social-graph.js`.
|
|
4
|
+
*
|
|
5
|
+
* Scope (CLI-first):
|
|
6
|
+
* - Pure functions over snapshots `{nodes, edges}` — no DB, no state
|
|
7
|
+
* - Callers feed a snapshot from `getGraphSnapshot()` (or any source with
|
|
8
|
+
* the same shape) and receive deterministic numeric scores keyed by DID
|
|
9
|
+
* - All metrics support optional edge-type filtering so analytics can be
|
|
10
|
+
* computed per relationship kind (e.g. only `follow` edges)
|
|
11
|
+
*
|
|
12
|
+
* What this module is NOT:
|
|
13
|
+
* - A visualization layer. Callers drive rendering from the numeric results.
|
|
14
|
+
* - A real-time stream. It operates on point-in-time snapshots. Callers that
|
|
15
|
+
* need live updates should subscribe to `social-graph.subscribe()` and
|
|
16
|
+
* re-run the relevant metric after each mutation batch.
|
|
17
|
+
* - An approximation service. Metrics are exact for the input; there is no
|
|
18
|
+
* sampling or Monte Carlo. Costs are O(n+m) to O(n*m) depending on the
|
|
19
|
+
* metric — fine for typical CLI-scale graphs (low thousands of nodes).
|
|
20
|
+
*
|
|
21
|
+
* Algorithms:
|
|
22
|
+
* - Degree: simple in/out/total counts, optionally normalized by (n-1)
|
|
23
|
+
* - Closeness: BFS from each node, harmonic mean of reciprocal distances
|
|
24
|
+
* (handles disconnected graphs without producing Infinity)
|
|
25
|
+
* - Betweenness: Brandes' algorithm (O(nm) for unweighted graphs)
|
|
26
|
+
* - Eigenvector: power iteration with L2 normalization, converges on
|
|
27
|
+
* strongly-connected components; otherwise converges to dominant one
|
|
28
|
+
* - Communities: label propagation with deterministic tie-breaks (sort
|
|
29
|
+
* by DID lexicographically); cheaper than full Louvain and matches
|
|
30
|
+
* the existing CLI's preference for deterministic output
|
|
31
|
+
* - Shortest path: BFS (unweighted); returns path of DIDs or `{found: false}`
|
|
32
|
+
* - Influence: weighted linear combination of normalized centralities
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/* ── Public catalog ───────────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
export const METRICS = Object.freeze([
|
|
38
|
+
"degree",
|
|
39
|
+
"closeness",
|
|
40
|
+
"betweenness",
|
|
41
|
+
"eigenvector",
|
|
42
|
+
"influence",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_INFLUENCE_WEIGHTS = Object.freeze({
|
|
46
|
+
degree: 0.25,
|
|
47
|
+
closeness: 0.25,
|
|
48
|
+
betweenness: 0.25,
|
|
49
|
+
eigenvector: 0.25,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/* ── Snapshot helpers ─────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
function _assertSnapshot(snapshot) {
|
|
55
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
56
|
+
throw new Error("snapshot must be an object with {nodes, edges}");
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(snapshot.nodes)) {
|
|
59
|
+
throw new Error("snapshot.nodes must be an array");
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(snapshot.edges)) {
|
|
62
|
+
throw new Error("snapshot.edges must be an array");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _filterEdges(edges, edgeTypes) {
|
|
67
|
+
if (!edgeTypes || edgeTypes.length === 0) return edges;
|
|
68
|
+
const set = new Set(edgeTypes);
|
|
69
|
+
return edges.filter((e) => set.has(e.edgeType));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Collect unique DIDs from nodes AND edges (some edges may reference DIDs
|
|
74
|
+
* that never appeared as standalone nodes if the caller curated nodes[]).
|
|
75
|
+
*/
|
|
76
|
+
function _allDids(snapshot, edges) {
|
|
77
|
+
const dids = new Set();
|
|
78
|
+
for (const n of snapshot.nodes) if (n && n.did) dids.add(n.did);
|
|
79
|
+
for (const e of edges) {
|
|
80
|
+
if (e.sourceDid) dids.add(e.sourceDid);
|
|
81
|
+
if (e.targetDid) dids.add(e.targetDid);
|
|
82
|
+
}
|
|
83
|
+
return [...dids].sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a directed adjacency map: Map<did, Map<neighborDid, totalWeight>>.
|
|
88
|
+
* Parallel edges of different types between the same pair collapse by
|
|
89
|
+
* summing weights (default 1 if edge.weight is missing).
|
|
90
|
+
*/
|
|
91
|
+
function _directedAdjacency(edges) {
|
|
92
|
+
const adj = new Map();
|
|
93
|
+
for (const e of edges) {
|
|
94
|
+
if (!e.sourceDid || !e.targetDid) continue;
|
|
95
|
+
if (!adj.has(e.sourceDid)) adj.set(e.sourceDid, new Map());
|
|
96
|
+
const bucket = adj.get(e.sourceDid);
|
|
97
|
+
const w = typeof e.weight === "number" ? e.weight : 1;
|
|
98
|
+
bucket.set(e.targetDid, (bucket.get(e.targetDid) || 0) + w);
|
|
99
|
+
}
|
|
100
|
+
return adj;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build an undirected adjacency map by summing weights in both directions.
|
|
105
|
+
*/
|
|
106
|
+
function _undirectedAdjacency(edges) {
|
|
107
|
+
const adj = new Map();
|
|
108
|
+
const add = (a, b, w) => {
|
|
109
|
+
if (!adj.has(a)) adj.set(a, new Map());
|
|
110
|
+
const bucket = adj.get(a);
|
|
111
|
+
bucket.set(b, (bucket.get(b) || 0) + w);
|
|
112
|
+
};
|
|
113
|
+
for (const e of edges) {
|
|
114
|
+
if (!e.sourceDid || !e.targetDid) continue;
|
|
115
|
+
if (e.sourceDid === e.targetDid) continue;
|
|
116
|
+
const w = typeof e.weight === "number" ? e.weight : 1;
|
|
117
|
+
add(e.sourceDid, e.targetDid, w);
|
|
118
|
+
add(e.targetDid, e.sourceDid, w);
|
|
119
|
+
}
|
|
120
|
+
return adj;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── Degree centrality ─────────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Degree centrality per DID.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} snapshot — {nodes, edges}
|
|
129
|
+
* @param {object} [opts]
|
|
130
|
+
* @param {"in"|"out"|"both"} [opts.direction="both"]
|
|
131
|
+
* @param {boolean} [opts.normalize=true] — divide by (n-1)
|
|
132
|
+
* @param {string[]} [opts.edgeTypes] — restrict to these edge types
|
|
133
|
+
* @returns {object} dict keyed by DID → score
|
|
134
|
+
*/
|
|
135
|
+
export function degreeCentrality(snapshot, opts = {}) {
|
|
136
|
+
_assertSnapshot(snapshot);
|
|
137
|
+
const { direction = "both", normalize = true, edgeTypes } = opts;
|
|
138
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
139
|
+
const dids = _allDids(snapshot, edges);
|
|
140
|
+
const scores = {};
|
|
141
|
+
for (const d of dids) scores[d] = 0;
|
|
142
|
+
|
|
143
|
+
for (const e of edges) {
|
|
144
|
+
if (!e.sourceDid || !e.targetDid) continue;
|
|
145
|
+
if (direction === "out" || direction === "both") {
|
|
146
|
+
scores[e.sourceDid] = (scores[e.sourceDid] || 0) + 1;
|
|
147
|
+
}
|
|
148
|
+
if (direction === "in" || direction === "both") {
|
|
149
|
+
scores[e.targetDid] = (scores[e.targetDid] || 0) + 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (normalize && dids.length > 1) {
|
|
154
|
+
const denom = dids.length - 1;
|
|
155
|
+
for (const d of dids) scores[d] = scores[d] / denom;
|
|
156
|
+
}
|
|
157
|
+
return scores;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Closeness centrality ─────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* BFS from `source` returning Map<did, distance>. Unreachable nodes are
|
|
164
|
+
* excluded from the map.
|
|
165
|
+
*/
|
|
166
|
+
function _bfsDistances(source, adj) {
|
|
167
|
+
const dist = new Map();
|
|
168
|
+
dist.set(source, 0);
|
|
169
|
+
const queue = [source];
|
|
170
|
+
let head = 0;
|
|
171
|
+
while (head < queue.length) {
|
|
172
|
+
const u = queue[head++];
|
|
173
|
+
const neighbors = adj.get(u);
|
|
174
|
+
if (!neighbors) continue;
|
|
175
|
+
for (const v of neighbors.keys()) {
|
|
176
|
+
if (!dist.has(v)) {
|
|
177
|
+
dist.set(v, dist.get(u) + 1);
|
|
178
|
+
queue.push(v);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return dist;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Closeness centrality (harmonic variant — handles disconnected graphs).
|
|
187
|
+
* C(v) = (1 / (n-1)) * sum_{u != v, reachable} 1/d(v, u)
|
|
188
|
+
*
|
|
189
|
+
* @param {object} snapshot
|
|
190
|
+
* @param {object} [opts]
|
|
191
|
+
* @param {boolean} [opts.directed=false] — if false, collapse to undirected
|
|
192
|
+
* @param {string[]} [opts.edgeTypes]
|
|
193
|
+
* @param {boolean} [opts.normalize=true] — divide by (n-1)
|
|
194
|
+
* @returns {object} dict DID → score
|
|
195
|
+
*/
|
|
196
|
+
export function closenessCentrality(snapshot, opts = {}) {
|
|
197
|
+
_assertSnapshot(snapshot);
|
|
198
|
+
const { directed = false, edgeTypes, normalize = true } = opts;
|
|
199
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
200
|
+
const dids = _allDids(snapshot, edges);
|
|
201
|
+
const adj = directed
|
|
202
|
+
? _directedAdjacency(edges)
|
|
203
|
+
: _undirectedAdjacency(edges);
|
|
204
|
+
|
|
205
|
+
const scores = {};
|
|
206
|
+
for (const d of dids) scores[d] = 0;
|
|
207
|
+
|
|
208
|
+
for (const source of dids) {
|
|
209
|
+
const dist = _bfsDistances(source, adj);
|
|
210
|
+
let sum = 0;
|
|
211
|
+
for (const [target, dd] of dist) {
|
|
212
|
+
if (target === source || dd === 0) continue;
|
|
213
|
+
sum += 1 / dd;
|
|
214
|
+
}
|
|
215
|
+
scores[source] = sum;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (normalize && dids.length > 1) {
|
|
219
|
+
const denom = dids.length - 1;
|
|
220
|
+
for (const d of dids) scores[d] = scores[d] / denom;
|
|
221
|
+
}
|
|
222
|
+
return scores;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ── Betweenness centrality (Brandes' algorithm) ──────────── */
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Betweenness centrality via Brandes' algorithm for unweighted graphs.
|
|
229
|
+
* O(V * E) time.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} snapshot
|
|
232
|
+
* @param {object} [opts]
|
|
233
|
+
* @param {boolean} [opts.directed=false]
|
|
234
|
+
* @param {string[]} [opts.edgeTypes]
|
|
235
|
+
* @param {boolean} [opts.normalize=true] — for undirected: 2/((n-1)(n-2))
|
|
236
|
+
* @returns {object} dict DID → score
|
|
237
|
+
*/
|
|
238
|
+
export function betweennessCentrality(snapshot, opts = {}) {
|
|
239
|
+
_assertSnapshot(snapshot);
|
|
240
|
+
const { directed = false, edgeTypes, normalize = true } = opts;
|
|
241
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
242
|
+
const dids = _allDids(snapshot, edges);
|
|
243
|
+
const adj = directed
|
|
244
|
+
? _directedAdjacency(edges)
|
|
245
|
+
: _undirectedAdjacency(edges);
|
|
246
|
+
|
|
247
|
+
const scores = {};
|
|
248
|
+
for (const d of dids) scores[d] = 0;
|
|
249
|
+
|
|
250
|
+
for (const s of dids) {
|
|
251
|
+
const stack = [];
|
|
252
|
+
const pred = new Map();
|
|
253
|
+
const sigma = new Map();
|
|
254
|
+
const dist = new Map();
|
|
255
|
+
for (const v of dids) {
|
|
256
|
+
pred.set(v, []);
|
|
257
|
+
sigma.set(v, 0);
|
|
258
|
+
dist.set(v, -1);
|
|
259
|
+
}
|
|
260
|
+
sigma.set(s, 1);
|
|
261
|
+
dist.set(s, 0);
|
|
262
|
+
|
|
263
|
+
const queue = [s];
|
|
264
|
+
let head = 0;
|
|
265
|
+
while (head < queue.length) {
|
|
266
|
+
const v = queue[head++];
|
|
267
|
+
stack.push(v);
|
|
268
|
+
const neighbors = adj.get(v);
|
|
269
|
+
if (!neighbors) continue;
|
|
270
|
+
for (const w of neighbors.keys()) {
|
|
271
|
+
if (dist.get(w) < 0) {
|
|
272
|
+
queue.push(w);
|
|
273
|
+
dist.set(w, dist.get(v) + 1);
|
|
274
|
+
}
|
|
275
|
+
if (dist.get(w) === dist.get(v) + 1) {
|
|
276
|
+
sigma.set(w, sigma.get(w) + sigma.get(v));
|
|
277
|
+
pred.get(w).push(v);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const delta = new Map();
|
|
283
|
+
for (const v of dids) delta.set(v, 0);
|
|
284
|
+
while (stack.length > 0) {
|
|
285
|
+
const w = stack.pop();
|
|
286
|
+
for (const v of pred.get(w)) {
|
|
287
|
+
const contrib = (sigma.get(v) / sigma.get(w)) * (1 + delta.get(w));
|
|
288
|
+
delta.set(v, delta.get(v) + contrib);
|
|
289
|
+
}
|
|
290
|
+
if (w !== s) scores[w] += delta.get(w);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// For undirected graphs, Brandes' formulation double-counts each pair.
|
|
295
|
+
if (!directed) {
|
|
296
|
+
for (const d of dids) scores[d] = scores[d] / 2;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (normalize && dids.length > 2) {
|
|
300
|
+
const n = dids.length;
|
|
301
|
+
const denom = directed ? (n - 1) * (n - 2) : ((n - 1) * (n - 2)) / 2;
|
|
302
|
+
if (denom > 0) {
|
|
303
|
+
for (const d of dids) scores[d] = scores[d] / denom;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return scores;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ── Eigenvector centrality ───────────────────────────────── */
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Eigenvector centrality via power iteration.
|
|
313
|
+
*
|
|
314
|
+
* @param {object} snapshot
|
|
315
|
+
* @param {object} [opts]
|
|
316
|
+
* @param {boolean} [opts.directed=false]
|
|
317
|
+
* @param {string[]} [opts.edgeTypes]
|
|
318
|
+
* @param {number} [opts.iterations=100]
|
|
319
|
+
* @param {number} [opts.tolerance=1e-6]
|
|
320
|
+
* @returns {object} dict DID → score (L2-normalized, non-negative)
|
|
321
|
+
*/
|
|
322
|
+
export function eigenvectorCentrality(snapshot, opts = {}) {
|
|
323
|
+
_assertSnapshot(snapshot);
|
|
324
|
+
const {
|
|
325
|
+
directed = false,
|
|
326
|
+
edgeTypes,
|
|
327
|
+
iterations = 100,
|
|
328
|
+
tolerance = 1e-6,
|
|
329
|
+
} = opts;
|
|
330
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
331
|
+
const dids = _allDids(snapshot, edges);
|
|
332
|
+
const adj = directed
|
|
333
|
+
? _directedAdjacency(edges)
|
|
334
|
+
: _undirectedAdjacency(edges);
|
|
335
|
+
|
|
336
|
+
if (dids.length === 0) return {};
|
|
337
|
+
|
|
338
|
+
// Initialize uniformly; converges to principal eigenvector.
|
|
339
|
+
let x = new Map();
|
|
340
|
+
const seed = 1 / Math.sqrt(dids.length);
|
|
341
|
+
for (const d of dids) x.set(d, seed);
|
|
342
|
+
|
|
343
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
344
|
+
const xNew = new Map();
|
|
345
|
+
for (const d of dids) xNew.set(d, 0);
|
|
346
|
+
|
|
347
|
+
for (const [u, neighbors] of adj) {
|
|
348
|
+
for (const [v, w] of neighbors) {
|
|
349
|
+
xNew.set(v, (xNew.get(v) || 0) + w * x.get(u));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Shift by I (add x to xNew) to break bipartite oscillation. Mathematically
|
|
354
|
+
// this yields the principal eigenvector of (I + A), which shares the same
|
|
355
|
+
// principal eigenvector direction as A for connected non-negative graphs
|
|
356
|
+
// but eliminates the +λ/-λ sign flip that causes power iteration to stall
|
|
357
|
+
// on bipartite structures (e.g. star graphs).
|
|
358
|
+
for (const d of dids) {
|
|
359
|
+
xNew.set(d, xNew.get(d) + x.get(d));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// L2 normalize; if zero vector, seed back uniformly.
|
|
363
|
+
let norm = 0;
|
|
364
|
+
for (const d of dids) norm += xNew.get(d) * xNew.get(d);
|
|
365
|
+
norm = Math.sqrt(norm);
|
|
366
|
+
if (norm === 0) {
|
|
367
|
+
for (const d of dids) xNew.set(d, seed);
|
|
368
|
+
} else {
|
|
369
|
+
for (const d of dids) xNew.set(d, xNew.get(d) / norm);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Convergence check
|
|
373
|
+
let diff = 0;
|
|
374
|
+
for (const d of dids) diff += Math.abs(xNew.get(d) - x.get(d));
|
|
375
|
+
x = xNew;
|
|
376
|
+
if (diff < tolerance) break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const scores = {};
|
|
380
|
+
for (const d of dids) scores[d] = Math.abs(x.get(d));
|
|
381
|
+
return scores;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* ── Influence score (composite) ──────────────────────────── */
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Composite influence score: weighted linear combination of the four
|
|
388
|
+
* centrality metrics, each normalized to [0, 1] before combining.
|
|
389
|
+
*
|
|
390
|
+
* @param {object} snapshot
|
|
391
|
+
* @param {object} [opts]
|
|
392
|
+
* @param {object} [opts.weights] — {degree, closeness, betweenness, eigenvector}
|
|
393
|
+
* @param {string[]} [opts.edgeTypes]
|
|
394
|
+
* @param {boolean} [opts.directed=false]
|
|
395
|
+
* @returns {object} dict DID → score in [0, 1]
|
|
396
|
+
*/
|
|
397
|
+
export function influenceScore(snapshot, opts = {}) {
|
|
398
|
+
_assertSnapshot(snapshot);
|
|
399
|
+
const { weights: userWeights, edgeTypes, directed = false } = opts;
|
|
400
|
+
const weights = { ...DEFAULT_INFLUENCE_WEIGHTS, ...(userWeights || {}) };
|
|
401
|
+
|
|
402
|
+
// Normalize weights so they sum to 1 (allows callers to pass raw ratios).
|
|
403
|
+
const wSum =
|
|
404
|
+
(weights.degree || 0) +
|
|
405
|
+
(weights.closeness || 0) +
|
|
406
|
+
(weights.betweenness || 0) +
|
|
407
|
+
(weights.eigenvector || 0);
|
|
408
|
+
if (wSum <= 0) {
|
|
409
|
+
throw new Error("influence weights must sum to a positive value");
|
|
410
|
+
}
|
|
411
|
+
const w = {
|
|
412
|
+
degree: (weights.degree || 0) / wSum,
|
|
413
|
+
closeness: (weights.closeness || 0) / wSum,
|
|
414
|
+
betweenness: (weights.betweenness || 0) / wSum,
|
|
415
|
+
eigenvector: (weights.eigenvector || 0) / wSum,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const passOpts = { edgeTypes, directed };
|
|
419
|
+
const deg = degreeCentrality(snapshot, { ...passOpts, normalize: true });
|
|
420
|
+
const close = closenessCentrality(snapshot, { ...passOpts, normalize: true });
|
|
421
|
+
const btw = betweennessCentrality(snapshot, { ...passOpts, normalize: true });
|
|
422
|
+
const eig = eigenvectorCentrality(snapshot, passOpts);
|
|
423
|
+
|
|
424
|
+
const dids = Object.keys(deg);
|
|
425
|
+
if (dids.length === 0) return {};
|
|
426
|
+
|
|
427
|
+
// Normalize each metric vector by its max so all land in [0, 1].
|
|
428
|
+
const _normalize = (m) => {
|
|
429
|
+
const values = dids.map((d) => m[d] || 0);
|
|
430
|
+
const max = Math.max(...values, 0);
|
|
431
|
+
if (max === 0) return dids.reduce((acc, d) => ((acc[d] = 0), acc), {});
|
|
432
|
+
const out = {};
|
|
433
|
+
for (const d of dids) out[d] = (m[d] || 0) / max;
|
|
434
|
+
return out;
|
|
435
|
+
};
|
|
436
|
+
const dN = _normalize(deg);
|
|
437
|
+
const cN = _normalize(close);
|
|
438
|
+
const bN = _normalize(btw);
|
|
439
|
+
const eN = _normalize(eig);
|
|
440
|
+
|
|
441
|
+
const out = {};
|
|
442
|
+
for (const d of dids) {
|
|
443
|
+
out[d] =
|
|
444
|
+
w.degree * dN[d] +
|
|
445
|
+
w.closeness * cN[d] +
|
|
446
|
+
w.betweenness * bN[d] +
|
|
447
|
+
w.eigenvector * eN[d];
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* ── Community detection (label propagation) ──────────────── */
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Label propagation community detection. Deterministic via
|
|
456
|
+
* lexicographic DID ordering + tie-breaking by smallest label.
|
|
457
|
+
*
|
|
458
|
+
* @param {object} snapshot
|
|
459
|
+
* @param {object} [opts]
|
|
460
|
+
* @param {string[]} [opts.edgeTypes]
|
|
461
|
+
* @param {number} [opts.maxIterations=20]
|
|
462
|
+
* @param {number} [opts.minSize=1] — filter out communities below this size
|
|
463
|
+
* @returns {{communities: Array, modularity: number}}
|
|
464
|
+
*/
|
|
465
|
+
export function detectCommunities(snapshot, opts = {}) {
|
|
466
|
+
_assertSnapshot(snapshot);
|
|
467
|
+
const { edgeTypes, maxIterations = 20, minSize = 1 } = opts;
|
|
468
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
469
|
+
const dids = _allDids(snapshot, edges);
|
|
470
|
+
const adj = _undirectedAdjacency(edges);
|
|
471
|
+
|
|
472
|
+
if (dids.length === 0) {
|
|
473
|
+
return { communities: [], modularity: 0 };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Initialize: each node is its own community, labeled by its DID.
|
|
477
|
+
const labels = new Map();
|
|
478
|
+
for (const d of dids) labels.set(d, d);
|
|
479
|
+
|
|
480
|
+
// Stable sort ensures deterministic traversal.
|
|
481
|
+
const sortedDids = [...dids].sort();
|
|
482
|
+
|
|
483
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
484
|
+
let changed = false;
|
|
485
|
+
for (const v of sortedDids) {
|
|
486
|
+
const neighbors = adj.get(v);
|
|
487
|
+
if (!neighbors || neighbors.size === 0) continue;
|
|
488
|
+
|
|
489
|
+
// Tally label frequencies (weighted).
|
|
490
|
+
const counts = new Map();
|
|
491
|
+
for (const [u, w] of neighbors) {
|
|
492
|
+
const lbl = labels.get(u);
|
|
493
|
+
counts.set(lbl, (counts.get(lbl) || 0) + w);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Pick the max-weight label; tie-break by smallest lexicographic label.
|
|
497
|
+
let best = labels.get(v);
|
|
498
|
+
let bestWeight = counts.get(best) || 0;
|
|
499
|
+
const sortedLabels = [...counts.keys()].sort();
|
|
500
|
+
for (const lbl of sortedLabels) {
|
|
501
|
+
const w = counts.get(lbl);
|
|
502
|
+
if (w > bestWeight) {
|
|
503
|
+
best = lbl;
|
|
504
|
+
bestWeight = w;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (best !== labels.get(v)) {
|
|
508
|
+
labels.set(v, best);
|
|
509
|
+
changed = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!changed) break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Group by label.
|
|
516
|
+
const groups = new Map();
|
|
517
|
+
for (const d of sortedDids) {
|
|
518
|
+
const lbl = labels.get(d);
|
|
519
|
+
if (!groups.has(lbl)) groups.set(lbl, []);
|
|
520
|
+
groups.get(lbl).push(d);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Assign stable community IDs (c0, c1, ...) sorted by first member DID.
|
|
524
|
+
const labelList = [...groups.keys()].sort((a, b) => {
|
|
525
|
+
const aFirst = groups.get(a)[0];
|
|
526
|
+
const bFirst = groups.get(b)[0];
|
|
527
|
+
return aFirst < bFirst ? -1 : aFirst > bFirst ? 1 : 0;
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const communities = [];
|
|
531
|
+
let idx = 0;
|
|
532
|
+
for (const lbl of labelList) {
|
|
533
|
+
const members = groups.get(lbl);
|
|
534
|
+
if (members.length < minSize) continue;
|
|
535
|
+
communities.push({
|
|
536
|
+
id: `c${idx++}`,
|
|
537
|
+
label: lbl,
|
|
538
|
+
members: [...members].sort(),
|
|
539
|
+
size: members.length,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Modularity: Q = sum_c [L_c / m - (k_c / 2m)^2]
|
|
544
|
+
let m = 0;
|
|
545
|
+
for (const [, neighbors] of adj) {
|
|
546
|
+
for (const [, w] of neighbors) m += w;
|
|
547
|
+
}
|
|
548
|
+
m = m / 2; // undirected
|
|
549
|
+
let Q = 0;
|
|
550
|
+
if (m > 0) {
|
|
551
|
+
for (const c of communities) {
|
|
552
|
+
const set = new Set(c.members);
|
|
553
|
+
let Lc = 0;
|
|
554
|
+
let kc = 0;
|
|
555
|
+
for (const v of c.members) {
|
|
556
|
+
const neighbors = adj.get(v);
|
|
557
|
+
if (!neighbors) continue;
|
|
558
|
+
for (const [u, w] of neighbors) {
|
|
559
|
+
kc += w;
|
|
560
|
+
if (set.has(u)) Lc += w;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
Lc = Lc / 2;
|
|
564
|
+
Q += Lc / m - Math.pow(kc / (2 * m), 2);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return { communities, modularity: Q };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* ── Shortest path (BFS) ──────────────────────────────────── */
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Unweighted shortest path between two DIDs.
|
|
575
|
+
*
|
|
576
|
+
* @param {object} snapshot
|
|
577
|
+
* @param {string} source
|
|
578
|
+
* @param {string} target
|
|
579
|
+
* @param {object} [opts]
|
|
580
|
+
* @param {boolean} [opts.directed=true]
|
|
581
|
+
* @param {string[]} [opts.edgeTypes]
|
|
582
|
+
* @returns {{found: boolean, distance?: number, path?: string[]}}
|
|
583
|
+
*/
|
|
584
|
+
export function shortestPath(snapshot, source, target, opts = {}) {
|
|
585
|
+
_assertSnapshot(snapshot);
|
|
586
|
+
if (!source || !target) {
|
|
587
|
+
return { found: false, reason: "missing source or target" };
|
|
588
|
+
}
|
|
589
|
+
if (source === target) {
|
|
590
|
+
return { found: true, distance: 0, path: [source] };
|
|
591
|
+
}
|
|
592
|
+
const { directed = true, edgeTypes } = opts;
|
|
593
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
594
|
+
const adj = directed
|
|
595
|
+
? _directedAdjacency(edges)
|
|
596
|
+
: _undirectedAdjacency(edges);
|
|
597
|
+
|
|
598
|
+
const parent = new Map();
|
|
599
|
+
parent.set(source, null);
|
|
600
|
+
const queue = [source];
|
|
601
|
+
let head = 0;
|
|
602
|
+
let found = false;
|
|
603
|
+
while (head < queue.length) {
|
|
604
|
+
const u = queue[head++];
|
|
605
|
+
if (u === target) {
|
|
606
|
+
found = true;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
const neighbors = adj.get(u);
|
|
610
|
+
if (!neighbors) continue;
|
|
611
|
+
for (const v of neighbors.keys()) {
|
|
612
|
+
if (!parent.has(v)) {
|
|
613
|
+
parent.set(v, u);
|
|
614
|
+
queue.push(v);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (!found) return { found: false };
|
|
619
|
+
|
|
620
|
+
// Reconstruct path.
|
|
621
|
+
const path = [];
|
|
622
|
+
let cur = target;
|
|
623
|
+
while (cur !== null && cur !== undefined) {
|
|
624
|
+
path.unshift(cur);
|
|
625
|
+
cur = parent.get(cur);
|
|
626
|
+
}
|
|
627
|
+
return { found: true, distance: path.length - 1, path };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* ── Top-N ranking ────────────────────────────────────────── */
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Rank DIDs by a named metric. Returns up to `limit` entries sorted
|
|
634
|
+
* descending by score, ties broken by lexicographic DID order.
|
|
635
|
+
*
|
|
636
|
+
* @param {object} snapshot
|
|
637
|
+
* @param {"degree"|"closeness"|"betweenness"|"eigenvector"|"influence"} metric
|
|
638
|
+
* @param {object} [opts]
|
|
639
|
+
* @param {number} [opts.limit=10]
|
|
640
|
+
* @param {string[]} [opts.edgeTypes]
|
|
641
|
+
* @param {boolean} [opts.directed=false]
|
|
642
|
+
* @param {object} [opts.weights] — only used when metric="influence"
|
|
643
|
+
* @returns {Array<{did: string, score: number}>}
|
|
644
|
+
*/
|
|
645
|
+
export function topByMetric(snapshot, metric, opts = {}) {
|
|
646
|
+
if (!METRICS.includes(metric)) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Unknown metric "${metric}"; expected one of ${METRICS.join(", ")}`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const { limit = 10 } = opts;
|
|
652
|
+
let scores;
|
|
653
|
+
switch (metric) {
|
|
654
|
+
case "degree":
|
|
655
|
+
scores = degreeCentrality(snapshot, opts);
|
|
656
|
+
break;
|
|
657
|
+
case "closeness":
|
|
658
|
+
scores = closenessCentrality(snapshot, opts);
|
|
659
|
+
break;
|
|
660
|
+
case "betweenness":
|
|
661
|
+
scores = betweennessCentrality(snapshot, opts);
|
|
662
|
+
break;
|
|
663
|
+
case "eigenvector":
|
|
664
|
+
scores = eigenvectorCentrality(snapshot, opts);
|
|
665
|
+
break;
|
|
666
|
+
case "influence":
|
|
667
|
+
scores = influenceScore(snapshot, opts);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
const entries = Object.entries(scores || {}).map(([did, score]) => ({
|
|
671
|
+
did,
|
|
672
|
+
score,
|
|
673
|
+
}));
|
|
674
|
+
entries.sort((a, b) => {
|
|
675
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
676
|
+
return a.did < b.did ? -1 : a.did > b.did ? 1 : 0;
|
|
677
|
+
});
|
|
678
|
+
return entries.slice(0, Math.max(0, limit));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* ── Summary stats ────────────────────────────────────────── */
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Convenience snapshot-level rollup: node/edge counts, density, and
|
|
685
|
+
* the top-5 scorer by influence.
|
|
686
|
+
*/
|
|
687
|
+
export function analyticsStats(snapshot, opts = {}) {
|
|
688
|
+
_assertSnapshot(snapshot);
|
|
689
|
+
const { edgeTypes } = opts;
|
|
690
|
+
const edges = _filterEdges(snapshot.edges, edgeTypes);
|
|
691
|
+
const dids = _allDids(snapshot, edges);
|
|
692
|
+
const n = dids.length;
|
|
693
|
+
const maxPossibleDirected = n * (n - 1);
|
|
694
|
+
const density =
|
|
695
|
+
maxPossibleDirected > 0 ? edges.length / maxPossibleDirected : 0;
|
|
696
|
+
|
|
697
|
+
const topInfluence =
|
|
698
|
+
n > 0 ? topByMetric(snapshot, "influence", { ...opts, limit: 5 }) : [];
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
nodeCount: n,
|
|
702
|
+
edgeCount: edges.length,
|
|
703
|
+
density,
|
|
704
|
+
topInfluence,
|
|
705
|
+
generatedAt: new Date().toISOString(),
|
|
706
|
+
};
|
|
707
|
+
}
|