openplanter 0.1.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.
Files changed (126) hide show
  1. package/README.md +210 -0
  2. package/dist/builder.d.ts +11 -0
  3. package/dist/builder.d.ts.map +1 -0
  4. package/dist/builder.js +179 -0
  5. package/dist/builder.js.map +1 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +548 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +51 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +114 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/credentials.d.ts +52 -0
  15. package/dist/credentials.d.ts.map +1 -0
  16. package/dist/credentials.js +371 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/demo.d.ts +26 -0
  19. package/dist/demo.d.ts.map +1 -0
  20. package/dist/demo.js +95 -0
  21. package/dist/demo.js.map +1 -0
  22. package/dist/engine.d.ts +91 -0
  23. package/dist/engine.d.ts.map +1 -0
  24. package/dist/engine.js +1036 -0
  25. package/dist/engine.js.map +1 -0
  26. package/dist/index.d.ts +30 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +39 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/investigation-tools/aph-holdings.d.ts +61 -0
  31. package/dist/investigation-tools/aph-holdings.d.ts.map +1 -0
  32. package/dist/investigation-tools/aph-holdings.js +459 -0
  33. package/dist/investigation-tools/aph-holdings.js.map +1 -0
  34. package/dist/investigation-tools/asic-officer-lookup.d.ts +42 -0
  35. package/dist/investigation-tools/asic-officer-lookup.d.ts.map +1 -0
  36. package/dist/investigation-tools/asic-officer-lookup.js +197 -0
  37. package/dist/investigation-tools/asic-officer-lookup.js.map +1 -0
  38. package/dist/investigation-tools/asx-calendar-fetcher.d.ts +42 -0
  39. package/dist/investigation-tools/asx-calendar-fetcher.d.ts.map +1 -0
  40. package/dist/investigation-tools/asx-calendar-fetcher.js +271 -0
  41. package/dist/investigation-tools/asx-calendar-fetcher.js.map +1 -0
  42. package/dist/investigation-tools/asx-parser.d.ts +66 -0
  43. package/dist/investigation-tools/asx-parser.d.ts.map +1 -0
  44. package/dist/investigation-tools/asx-parser.js +314 -0
  45. package/dist/investigation-tools/asx-parser.js.map +1 -0
  46. package/dist/investigation-tools/bulk-asx-announcements.d.ts +53 -0
  47. package/dist/investigation-tools/bulk-asx-announcements.d.ts.map +1 -0
  48. package/dist/investigation-tools/bulk-asx-announcements.js +204 -0
  49. package/dist/investigation-tools/bulk-asx-announcements.js.map +1 -0
  50. package/dist/investigation-tools/entity-resolver.d.ts +77 -0
  51. package/dist/investigation-tools/entity-resolver.d.ts.map +1 -0
  52. package/dist/investigation-tools/entity-resolver.js +346 -0
  53. package/dist/investigation-tools/entity-resolver.js.map +1 -0
  54. package/dist/investigation-tools/hotcopper-scraper.d.ts +73 -0
  55. package/dist/investigation-tools/hotcopper-scraper.d.ts.map +1 -0
  56. package/dist/investigation-tools/hotcopper-scraper.js +318 -0
  57. package/dist/investigation-tools/hotcopper-scraper.js.map +1 -0
  58. package/dist/investigation-tools/index.d.ts +15 -0
  59. package/dist/investigation-tools/index.d.ts.map +1 -0
  60. package/dist/investigation-tools/index.js +15 -0
  61. package/dist/investigation-tools/index.js.map +1 -0
  62. package/dist/investigation-tools/insider-graph.d.ts +173 -0
  63. package/dist/investigation-tools/insider-graph.d.ts.map +1 -0
  64. package/dist/investigation-tools/insider-graph.js +732 -0
  65. package/dist/investigation-tools/insider-graph.js.map +1 -0
  66. package/dist/investigation-tools/insider-suspicion-scorer.d.ts +97 -0
  67. package/dist/investigation-tools/insider-suspicion-scorer.d.ts.map +1 -0
  68. package/dist/investigation-tools/insider-suspicion-scorer.js +327 -0
  69. package/dist/investigation-tools/insider-suspicion-scorer.js.map +1 -0
  70. package/dist/investigation-tools/multi-forum-scraper.d.ts +104 -0
  71. package/dist/investigation-tools/multi-forum-scraper.d.ts.map +1 -0
  72. package/dist/investigation-tools/multi-forum-scraper.js +415 -0
  73. package/dist/investigation-tools/multi-forum-scraper.js.map +1 -0
  74. package/dist/investigation-tools/price-fetcher.d.ts +81 -0
  75. package/dist/investigation-tools/price-fetcher.d.ts.map +1 -0
  76. package/dist/investigation-tools/price-fetcher.js +268 -0
  77. package/dist/investigation-tools/price-fetcher.js.map +1 -0
  78. package/dist/investigation-tools/shared.d.ts +39 -0
  79. package/dist/investigation-tools/shared.d.ts.map +1 -0
  80. package/dist/investigation-tools/shared.js +203 -0
  81. package/dist/investigation-tools/shared.js.map +1 -0
  82. package/dist/investigation-tools/timeline-linker.d.ts +90 -0
  83. package/dist/investigation-tools/timeline-linker.d.ts.map +1 -0
  84. package/dist/investigation-tools/timeline-linker.js +219 -0
  85. package/dist/investigation-tools/timeline-linker.js.map +1 -0
  86. package/dist/investigation-tools/volume-scanner.d.ts +70 -0
  87. package/dist/investigation-tools/volume-scanner.d.ts.map +1 -0
  88. package/dist/investigation-tools/volume-scanner.js +227 -0
  89. package/dist/investigation-tools/volume-scanner.js.map +1 -0
  90. package/dist/model.d.ts +136 -0
  91. package/dist/model.d.ts.map +1 -0
  92. package/dist/model.js +1071 -0
  93. package/dist/model.js.map +1 -0
  94. package/dist/patching.d.ts +45 -0
  95. package/dist/patching.d.ts.map +1 -0
  96. package/dist/patching.js +317 -0
  97. package/dist/patching.js.map +1 -0
  98. package/dist/prompts.d.ts +15 -0
  99. package/dist/prompts.d.ts.map +1 -0
  100. package/dist/prompts.js +351 -0
  101. package/dist/prompts.js.map +1 -0
  102. package/dist/replay-log.d.ts +54 -0
  103. package/dist/replay-log.d.ts.map +1 -0
  104. package/dist/replay-log.js +94 -0
  105. package/dist/replay-log.js.map +1 -0
  106. package/dist/runtime.d.ts +53 -0
  107. package/dist/runtime.d.ts.map +1 -0
  108. package/dist/runtime.js +259 -0
  109. package/dist/runtime.js.map +1 -0
  110. package/dist/settings.d.ts +39 -0
  111. package/dist/settings.d.ts.map +1 -0
  112. package/dist/settings.js +146 -0
  113. package/dist/settings.js.map +1 -0
  114. package/dist/tool-defs.d.ts +58 -0
  115. package/dist/tool-defs.d.ts.map +1 -0
  116. package/dist/tool-defs.js +1029 -0
  117. package/dist/tool-defs.js.map +1 -0
  118. package/dist/tools.d.ts +72 -0
  119. package/dist/tools.d.ts.map +1 -0
  120. package/dist/tools.js +1454 -0
  121. package/dist/tools.js.map +1 -0
  122. package/dist/tui.d.ts +49 -0
  123. package/dist/tui.d.ts.map +1 -0
  124. package/dist/tui.js +699 -0
  125. package/dist/tui.js.map +1 -0
  126. package/package.json +126 -0
@@ -0,0 +1,732 @@
1
+ /**
2
+ * insider-graph.ts — Entity-event relationship graph builder for insider trading investigations.
3
+ *
4
+ * Builds directed graphs of people, companies, events, and tickers with weighted edges
5
+ * representing relationships (shareholdings, directorships, trades, announcements, etc.).
6
+ * Supports pathfinding (Dijkstra), neighbourhood queries, community detection, and
7
+ * suspicion-path analysis connecting trade events to information sources.
8
+ *
9
+ * Migrated from tools/insider_graph.py — all graph algorithms implemented from scratch
10
+ * with no external dependencies.
11
+ */
12
+ // ── Constants ──────────────────────────────────────────────────
13
+ export const VALID_NODE_TYPES = new Set(["person", "company", "event", "ticker"]);
14
+ export const EDGE_TYPE_DEFAULTS = {
15
+ holds_shares: 1,
16
+ is_director: 1,
17
+ traded: 2,
18
+ announced: 1,
19
+ related_to: 3,
20
+ same_entity: 0,
21
+ lobbied_by: 2,
22
+ committee_member: 2,
23
+ };
24
+ /** Event sub-types treated as "information sources" for suspicion analysis. */
25
+ export const INFO_SOURCE_EVENT_SUBTYPES = new Set([
26
+ "announcement",
27
+ "report",
28
+ "meeting",
29
+ ]);
30
+ class MinHeap {
31
+ heap = [];
32
+ get size() {
33
+ return this.heap.length;
34
+ }
35
+ push(key, value) {
36
+ this.heap.push({ key, value });
37
+ this.bubbleUp(this.heap.length - 1);
38
+ }
39
+ pop() {
40
+ if (this.heap.length === 0)
41
+ return undefined;
42
+ const top = this.heap[0];
43
+ const last = this.heap.pop();
44
+ if (this.heap.length > 0) {
45
+ this.heap[0] = last;
46
+ this.sinkDown(0);
47
+ }
48
+ return top;
49
+ }
50
+ bubbleUp(idx) {
51
+ while (idx > 0) {
52
+ const parent = (idx - 1) >> 1;
53
+ if (this.heap[parent].key <= this.heap[idx].key)
54
+ break;
55
+ [this.heap[parent], this.heap[idx]] = [this.heap[idx], this.heap[parent]];
56
+ idx = parent;
57
+ }
58
+ }
59
+ sinkDown(idx) {
60
+ const len = this.heap.length;
61
+ for (;;) {
62
+ let smallest = idx;
63
+ const left = 2 * idx + 1;
64
+ const right = 2 * idx + 2;
65
+ if (left < len && this.heap[left].key < this.heap[smallest].key)
66
+ smallest = left;
67
+ if (right < len && this.heap[right].key < this.heap[smallest].key)
68
+ smallest = right;
69
+ if (smallest === idx)
70
+ break;
71
+ [this.heap[smallest], this.heap[idx]] = [this.heap[idx], this.heap[smallest]];
72
+ idx = smallest;
73
+ }
74
+ }
75
+ }
76
+ // ── Internal helpers ───────────────────────────────────────────
77
+ function getEdgeWeight(edata) {
78
+ return typeof edata?.weight === "number" ? edata.weight : 1;
79
+ }
80
+ function undirectedEdgeData(G, u, v) {
81
+ return G.getEdgeData(u, v) ?? G.getEdgeData(v, u) ?? {};
82
+ }
83
+ function nodeStr(attrs, key, fallback) {
84
+ const v = attrs?.[key];
85
+ return typeof v === "string" ? v : fallback;
86
+ }
87
+ function nodeOptStr(attrs, key) {
88
+ const v = attrs?.[key];
89
+ return typeof v === "string" ? v : undefined;
90
+ }
91
+ // ── DiGraph class ──────────────────────────────────────────────
92
+ /**
93
+ * Lightweight directed graph with adjacency-list storage.
94
+ *
95
+ * Stores node attributes and edge attributes. Supports both directed
96
+ * and undirected-view operations required for insider-graph analysis.
97
+ */
98
+ export class DiGraph {
99
+ _nodes = new Map();
100
+ _adj = new Map();
101
+ _pred = new Map();
102
+ // ── Mutation ───────────────────────────────────────────────
103
+ /** Add a node (or update its attributes if it already exists). */
104
+ addNode(id, attrs = {}) {
105
+ const existing = this._nodes.get(id);
106
+ if (existing) {
107
+ Object.assign(existing, attrs);
108
+ }
109
+ else {
110
+ this._nodes.set(id, { ...attrs });
111
+ if (!this._adj.has(id))
112
+ this._adj.set(id, new Map());
113
+ if (!this._pred.has(id))
114
+ this._pred.set(id, new Set());
115
+ }
116
+ }
117
+ /** Add a directed edge (or update its attributes if it already exists). */
118
+ addEdge(source, target, attrs = {}) {
119
+ if (!this._nodes.has(source))
120
+ this.addNode(source);
121
+ if (!this._nodes.has(target))
122
+ this.addNode(target);
123
+ const fwd = this._adj.get(source);
124
+ const existing = fwd.get(target);
125
+ if (existing) {
126
+ Object.assign(existing, attrs);
127
+ }
128
+ else {
129
+ fwd.set(target, { ...attrs });
130
+ }
131
+ this._pred.get(target).add(source);
132
+ }
133
+ // ── Queries ────────────────────────────────────────────────
134
+ hasNode(id) {
135
+ return this._nodes.has(id);
136
+ }
137
+ getNodeData(id) {
138
+ return this._nodes.get(id);
139
+ }
140
+ getEdgeData(u, v) {
141
+ return this._adj.get(u)?.get(v);
142
+ }
143
+ numberOfNodes() {
144
+ return this._nodes.size;
145
+ }
146
+ numberOfEdges() {
147
+ let count = 0;
148
+ for (const targets of this._adj.values())
149
+ count += targets.size;
150
+ return count;
151
+ }
152
+ /** All node IDs. */
153
+ nodeIds() {
154
+ return [...this._nodes.keys()];
155
+ }
156
+ /** Iterate over nodes with their attribute dicts. */
157
+ nodesWithData() {
158
+ return [...this._nodes.entries()];
159
+ }
160
+ /** Iterate over edges as [source, target, attrs] triples. */
161
+ edgesWithData() {
162
+ const result = [];
163
+ for (const [src, targets] of this._adj) {
164
+ for (const [tgt, attrs] of targets) {
165
+ result.push([src, tgt, attrs]);
166
+ }
167
+ }
168
+ return result;
169
+ }
170
+ /** Outgoing neighbours (directed). */
171
+ successors(node) {
172
+ const adj = this._adj.get(node);
173
+ return adj ? [...adj.keys()] : [];
174
+ }
175
+ /** Incoming neighbours (directed). */
176
+ predecessors(node) {
177
+ const pred = this._pred.get(node);
178
+ return pred ? [...pred] : [];
179
+ }
180
+ /** All neighbours (undirected view — union of successors and predecessors). */
181
+ undirectedNeighbors(node) {
182
+ const nbrs = new Set();
183
+ const adj = this._adj.get(node);
184
+ if (adj)
185
+ for (const n of adj.keys())
186
+ nbrs.add(n);
187
+ const pred = this._pred.get(node);
188
+ if (pred)
189
+ for (const n of pred)
190
+ nbrs.add(n);
191
+ return [...nbrs];
192
+ }
193
+ }
194
+ // ── Graph algorithms (internal) ────────────────────────────────
195
+ /**
196
+ * Dijkstra shortest path on the undirected view of G.
197
+ * Returns {path, length} or null if no path exists.
198
+ */
199
+ function dijkstraPath(G, source, target) {
200
+ if (!G.hasNode(source) || !G.hasNode(target))
201
+ return null;
202
+ const dist = new Map();
203
+ const prev = new Map();
204
+ const visited = new Set();
205
+ const heap = new MinHeap();
206
+ dist.set(source, 0);
207
+ prev.set(source, null);
208
+ heap.push(0, source);
209
+ while (heap.size > 0) {
210
+ const { key: d, value: u } = heap.pop();
211
+ if (visited.has(u))
212
+ continue;
213
+ visited.add(u);
214
+ if (u === target) {
215
+ const path = [];
216
+ let cur = target;
217
+ while (cur != null) {
218
+ path.unshift(cur);
219
+ cur = prev.get(cur) ?? null;
220
+ if (cur === null)
221
+ break; // reached source
222
+ }
223
+ return { path, length: d };
224
+ }
225
+ for (const nbr of G.undirectedNeighbors(u)) {
226
+ if (visited.has(nbr))
227
+ continue;
228
+ const edata = undirectedEdgeData(G, u, nbr);
229
+ const w = getEdgeWeight(edata);
230
+ const newDist = d + w;
231
+ const oldDist = dist.get(nbr);
232
+ if (oldDist === undefined || newDist < oldDist) {
233
+ dist.set(nbr, newDist);
234
+ prev.set(nbr, u);
235
+ heap.push(newDist, nbr);
236
+ }
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ /** Connected components on the undirected view (BFS). */
242
+ function connectedComponents(G) {
243
+ const visited = new Set();
244
+ const components = [];
245
+ for (const node of G.nodeIds()) {
246
+ if (visited.has(node))
247
+ continue;
248
+ const component = [];
249
+ const queue = [node];
250
+ visited.add(node);
251
+ while (queue.length > 0) {
252
+ const cur = queue.shift();
253
+ component.push(cur);
254
+ for (const nbr of G.undirectedNeighbors(cur)) {
255
+ if (!visited.has(nbr)) {
256
+ visited.add(nbr);
257
+ queue.push(nbr);
258
+ }
259
+ }
260
+ }
261
+ components.push(component);
262
+ }
263
+ return components;
264
+ }
265
+ /**
266
+ * Betweenness centrality on the undirected view (Brandes' algorithm with weights).
267
+ * Returns a map of node ID → centrality score (normalised).
268
+ */
269
+ function betweennessCentrality(G) {
270
+ const nodes = G.nodeIds();
271
+ const n = nodes.length;
272
+ const centrality = new Map();
273
+ for (const nd of nodes)
274
+ centrality.set(nd, 0);
275
+ for (const s of nodes) {
276
+ // Weighted single-source shortest-path (Dijkstra)
277
+ const stack = [];
278
+ const pred = new Map();
279
+ for (const nd of nodes)
280
+ pred.set(nd, []);
281
+ const sigma = new Map();
282
+ for (const nd of nodes)
283
+ sigma.set(nd, 0);
284
+ sigma.set(s, 1);
285
+ const dist = new Map();
286
+ for (const nd of nodes)
287
+ dist.set(nd, Infinity);
288
+ dist.set(s, 0);
289
+ const heap = new MinHeap();
290
+ heap.push(0, s);
291
+ while (heap.size > 0) {
292
+ const { key: d, value: v } = heap.pop();
293
+ if (d > dist.get(v))
294
+ continue;
295
+ stack.push(v);
296
+ for (const w of G.undirectedNeighbors(v)) {
297
+ const edata = undirectedEdgeData(G, v, w);
298
+ const ew = getEdgeWeight(edata);
299
+ const newDist = d + ew;
300
+ const wDist = dist.get(w);
301
+ if (newDist < wDist) {
302
+ dist.set(w, newDist);
303
+ sigma.set(w, 0);
304
+ pred.set(w, [v]);
305
+ sigma.set(w, sigma.get(v));
306
+ heap.push(newDist, w);
307
+ }
308
+ else if (Math.abs(newDist - wDist) < 1e-10) {
309
+ sigma.set(w, sigma.get(w) + sigma.get(v));
310
+ pred.get(w).push(v);
311
+ }
312
+ }
313
+ }
314
+ // Accumulation
315
+ const delta = new Map();
316
+ for (const nd of nodes)
317
+ delta.set(nd, 0);
318
+ while (stack.length > 0) {
319
+ const w = stack.pop();
320
+ for (const v of pred.get(w)) {
321
+ const sigmaV = sigma.get(v);
322
+ const sigmaW = sigma.get(w);
323
+ if (sigmaW > 0) {
324
+ delta.set(v, delta.get(v) + (sigmaV / sigmaW) * (1 + delta.get(w)));
325
+ }
326
+ }
327
+ if (w !== s) {
328
+ centrality.set(w, centrality.get(w) + delta.get(w));
329
+ }
330
+ }
331
+ }
332
+ // Normalise for undirected graph: multiply by 2 / ((n-1)(n-2))
333
+ if (n > 2) {
334
+ const norm = 2 / ((n - 1) * (n - 2));
335
+ for (const [nd, val] of centrality) {
336
+ centrality.set(nd, val * norm);
337
+ }
338
+ }
339
+ return centrality;
340
+ }
341
+ /** Diameter of the undirected view (max shortest-path hop-count). Unweighted BFS. */
342
+ function computeDiameter(G) {
343
+ let maxDist = 0;
344
+ for (const s of G.nodeIds()) {
345
+ const dist = new Map();
346
+ dist.set(s, 0);
347
+ const queue = [s];
348
+ while (queue.length > 0) {
349
+ const u = queue.shift();
350
+ const uDist = dist.get(u);
351
+ for (const nbr of G.undirectedNeighbors(u)) {
352
+ if (!dist.has(nbr)) {
353
+ const d = uDist + 1;
354
+ dist.set(nbr, d);
355
+ if (d > maxDist)
356
+ maxDist = d;
357
+ queue.push(nbr);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ return maxDist;
363
+ }
364
+ // ── Graph construction ─────────────────────────────────────────
365
+ /** Construct a DiGraph from the canonical input JSON schema. */
366
+ export function buildGraph(data) {
367
+ const G = new DiGraph();
368
+ for (const node of data.nodes ?? []) {
369
+ const attrs = {};
370
+ attrs.type = node.type ?? "unknown";
371
+ attrs.label = node.label ?? node.id;
372
+ // Promote subtype from either top-level or properties.
373
+ const subtype = node.subtype ?? node.properties?.subtype;
374
+ if (subtype)
375
+ attrs.subtype = subtype;
376
+ // Merge remaining properties (skip keys already set).
377
+ if (node.properties) {
378
+ for (const [k, v] of Object.entries(node.properties)) {
379
+ if (!(k in attrs))
380
+ attrs[k] = v;
381
+ }
382
+ }
383
+ G.addNode(node.id, attrs);
384
+ }
385
+ for (const edge of data.edges ?? []) {
386
+ const attrs = {};
387
+ attrs.type = edge.type ?? "unknown";
388
+ attrs.weight =
389
+ edge.weight ?? EDGE_TYPE_DEFAULTS[attrs.type] ?? 1;
390
+ if (edge.properties) {
391
+ for (const [k, v] of Object.entries(edge.properties)) {
392
+ if (!(k in attrs))
393
+ attrs[k] = v;
394
+ }
395
+ }
396
+ G.addEdge(edge.source, edge.target, attrs);
397
+ }
398
+ return G;
399
+ }
400
+ // ── Query functions ────────────────────────────────────────────
401
+ /** Dijkstra shortest path between source and target (undirected view). */
402
+ export function findShortestPath(G, source, target) {
403
+ if (!G.hasNode(source)) {
404
+ return {
405
+ found: false,
406
+ source,
407
+ target,
408
+ error: `Node '${source}' not in graph`,
409
+ path: [],
410
+ length: null,
411
+ };
412
+ }
413
+ if (!G.hasNode(target)) {
414
+ return {
415
+ found: false,
416
+ source,
417
+ target,
418
+ error: `Node '${target}' not in graph`,
419
+ path: [],
420
+ length: null,
421
+ };
422
+ }
423
+ const result = dijkstraPath(G, source, target);
424
+ if (!result) {
425
+ return { found: false, source, target, path: [], length: null };
426
+ }
427
+ const edges = [];
428
+ for (let i = 0; i < result.path.length - 1; i++) {
429
+ const u = result.path[i];
430
+ const v = result.path[i + 1];
431
+ const edata = undirectedEdgeData(G, u, v);
432
+ edges.push({
433
+ from: u,
434
+ to: v,
435
+ type: nodeOptStr(edata, "type"),
436
+ weight: typeof edata.weight === "number" ? edata.weight : undefined,
437
+ });
438
+ }
439
+ return {
440
+ found: true,
441
+ source,
442
+ target,
443
+ path: result.path,
444
+ length: result.length,
445
+ edges,
446
+ };
447
+ }
448
+ /** All nodes reachable within `depth` hops (undirected neighbourhood, BFS). */
449
+ export function findConnections(G, node, depth = 2) {
450
+ if (!G.hasNode(node)) {
451
+ return {
452
+ node,
453
+ error: `Node '${node}' not in graph`,
454
+ connections: [],
455
+ };
456
+ }
457
+ const visited = new Map();
458
+ let frontier = new Set([node]);
459
+ for (let d = 1; d <= depth; d++) {
460
+ const nextFrontier = new Set();
461
+ for (const n of frontier) {
462
+ for (const nbr of G.undirectedNeighbors(n)) {
463
+ if (!visited.has(nbr) && nbr !== node) {
464
+ visited.set(nbr, d);
465
+ nextFrontier.add(nbr);
466
+ }
467
+ }
468
+ }
469
+ frontier = nextFrontier;
470
+ }
471
+ const connections = [...visited.entries()]
472
+ .sort((a, b) => a[1] - b[1])
473
+ .map(([nid, dist]) => {
474
+ const ndata = G.getNodeData(nid);
475
+ return {
476
+ id: nid,
477
+ type: nodeOptStr(ndata, "type"),
478
+ label: nodeStr(ndata, "label", nid),
479
+ distance: dist,
480
+ };
481
+ });
482
+ return { node, depth, total: connections.length, connections };
483
+ }
484
+ /** Connected components (on the undirected projection) as clusters. */
485
+ export function findClusters(G) {
486
+ const components = connectedComponents(G);
487
+ // Sort by size descending
488
+ components.sort((a, b) => b.length - a.length);
489
+ const clusters = components.map((comp, i) => {
490
+ const members = [...comp]
491
+ .sort()
492
+ .map((nid) => {
493
+ const ndata = G.getNodeData(nid);
494
+ return {
495
+ id: nid,
496
+ type: nodeOptStr(ndata, "type"),
497
+ label: nodeStr(ndata, "label", nid),
498
+ };
499
+ });
500
+ return { cluster_id: i, size: comp.length, members };
501
+ });
502
+ return { total_clusters: clusters.length, clusters };
503
+ }
504
+ /**
505
+ * Find paths connecting a trade event to any information-source event
506
+ * (announcement, report, meeting). Highlights how an insider may have
507
+ * received material non-public information before executing a trade.
508
+ */
509
+ export function suspicionAnalysis(G, tradeEventId) {
510
+ if (!G.hasNode(tradeEventId)) {
511
+ return {
512
+ trade_event: tradeEventId,
513
+ error: `Node '${tradeEventId}' not in graph`,
514
+ paths: [],
515
+ };
516
+ }
517
+ // Identify information source nodes.
518
+ const infoSources = [];
519
+ for (const [nid, attrs] of G.nodesWithData()) {
520
+ if (attrs.type === "event" &&
521
+ typeof attrs.subtype === "string" &&
522
+ INFO_SOURCE_EVENT_SUBTYPES.has(attrs.subtype)) {
523
+ infoSources.push(nid);
524
+ }
525
+ }
526
+ const paths = [];
527
+ for (const srcId of infoSources) {
528
+ const result = dijkstraPath(G, tradeEventId, srcId);
529
+ if (!result)
530
+ continue;
531
+ const edges = [];
532
+ for (let i = 0; i < result.path.length - 1; i++) {
533
+ const u = result.path[i];
534
+ const v = result.path[i + 1];
535
+ const edata = undirectedEdgeData(G, u, v);
536
+ edges.push({
537
+ from: u,
538
+ to: v,
539
+ type: nodeOptStr(edata, "type"),
540
+ weight: typeof edata.weight === "number" ? edata.weight : undefined,
541
+ });
542
+ }
543
+ const srcData = G.getNodeData(srcId);
544
+ paths.push({
545
+ info_source: srcId,
546
+ info_source_label: nodeStr(srcData, "label", srcId),
547
+ path: result.path,
548
+ length: result.length,
549
+ edges,
550
+ });
551
+ }
552
+ // Sort by path length ascending.
553
+ paths.sort((a, b) => a.length - b.length);
554
+ return {
555
+ trade_event: tradeEventId,
556
+ total_paths: paths.length,
557
+ paths,
558
+ };
559
+ }
560
+ // ── Graph statistics ───────────────────────────────────────────
561
+ /** Compute summary statistics for the graph. */
562
+ export function graphStats(G) {
563
+ // Count node types.
564
+ const nodeTypes = {};
565
+ for (const [, attrs] of G.nodesWithData()) {
566
+ const t = typeof attrs.type === "string" ? attrs.type : "unknown";
567
+ nodeTypes[t] = (nodeTypes[t] ?? 0) + 1;
568
+ }
569
+ // Count edge types.
570
+ const edgeTypes = {};
571
+ for (const [, , attrs] of G.edgesWithData()) {
572
+ const t = typeof attrs.type === "string" ? attrs.type : "unknown";
573
+ edgeTypes[t] = (edgeTypes[t] ?? 0) + 1;
574
+ }
575
+ // Connected components.
576
+ const components = connectedComponents(G);
577
+ // Diameter — only meaningful for connected graphs (unweighted BFS).
578
+ let diameter = null;
579
+ if (components.length === 1 && G.numberOfNodes() > 1) {
580
+ try {
581
+ diameter = computeDiameter(G);
582
+ }
583
+ catch {
584
+ // leave null
585
+ }
586
+ }
587
+ // Betweenness centrality — top-5 nodes.
588
+ const centrality = betweennessCentrality(G);
589
+ const topCentral = [...centrality.entries()]
590
+ .sort((a, b) => b[1] - a[1])
591
+ .slice(0, 5);
592
+ const keyCentralNodes = topCentral
593
+ .filter(([, c]) => c > 0)
594
+ .map(([nid, c]) => {
595
+ const ndata = G.getNodeData(nid);
596
+ return {
597
+ id: nid,
598
+ label: nodeStr(ndata, "label", nid),
599
+ type: nodeOptStr(ndata, "type"),
600
+ centrality: Math.round(c * 10000) / 10000,
601
+ };
602
+ });
603
+ // Density for directed graph: m / (n * (n - 1))
604
+ const n = G.numberOfNodes();
605
+ const m = G.numberOfEdges();
606
+ const density = n > 1
607
+ ? Math.round((m / (n * (n - 1))) * 1000000) / 1000000
608
+ : 0;
609
+ return {
610
+ total_nodes: n,
611
+ total_edges: m,
612
+ node_types: nodeTypes,
613
+ edge_types: edgeTypes,
614
+ density,
615
+ connected_components: components.length,
616
+ diameter,
617
+ key_central_nodes: keyCentralNodes,
618
+ };
619
+ }
620
+ // ── Export functions ────────────────────────────────────────────
621
+ /** Node-link JSON suitable for D3.js force-directed graphs. */
622
+ export function exportJson(G) {
623
+ const data = {
624
+ directed: true,
625
+ multigraph: false,
626
+ graph: {},
627
+ nodes: G.nodesWithData().map(([id, attrs]) => ({ id, ...attrs })),
628
+ links: G.edgesWithData().map(([src, tgt, attrs]) => ({
629
+ source: src,
630
+ target: tgt,
631
+ ...attrs,
632
+ })),
633
+ };
634
+ return JSON.stringify(data, null, 2);
635
+ }
636
+ /** Escape special characters for XML content. */
637
+ function escapeXml(s) {
638
+ return s
639
+ .replace(/&/g, "&amp;")
640
+ .replace(/</g, "&lt;")
641
+ .replace(/>/g, "&gt;")
642
+ .replace(/"/g, "&quot;")
643
+ .replace(/'/g, "&apos;");
644
+ }
645
+ /** Infer a GraphML type string from a JS value. */
646
+ function graphmlType(value) {
647
+ if (typeof value === "number") {
648
+ return Number.isInteger(value) ? "int" : "double";
649
+ }
650
+ if (typeof value === "boolean")
651
+ return "boolean";
652
+ return "string";
653
+ }
654
+ /** GraphML format for Gephi / yEd. */
655
+ export function exportGraphml(G) {
656
+ // Collect all unique attribute keys and their types.
657
+ const nodeAttrKeys = new Map(); // name → graphml type
658
+ const edgeAttrKeys = new Map();
659
+ for (const [, attrs] of G.nodesWithData()) {
660
+ for (const [k, v] of Object.entries(attrs)) {
661
+ if (!nodeAttrKeys.has(k)) {
662
+ nodeAttrKeys.set(k, graphmlType(v));
663
+ }
664
+ }
665
+ }
666
+ for (const [, , attrs] of G.edgesWithData()) {
667
+ for (const [k, v] of Object.entries(attrs)) {
668
+ if (!edgeAttrKeys.has(k)) {
669
+ edgeAttrKeys.set(k, graphmlType(v));
670
+ }
671
+ }
672
+ }
673
+ // Assign key IDs.
674
+ const keyDefs = [];
675
+ const keyMap = new Map(); // "node:name" | "edge:name" → id
676
+ let keyIdx = 0;
677
+ for (const [name, type] of nodeAttrKeys) {
678
+ const id = `d${keyIdx++}`;
679
+ keyDefs.push({ id, for_: "node", name, type });
680
+ keyMap.set(`node:${name}`, id);
681
+ }
682
+ for (const [name, type] of edgeAttrKeys) {
683
+ const id = `d${keyIdx++}`;
684
+ keyDefs.push({ id, for_: "edge", name, type });
685
+ keyMap.set(`edge:${name}`, id);
686
+ }
687
+ // Build XML.
688
+ const lines = [
689
+ '<?xml version="1.0" encoding="UTF-8"?>',
690
+ '<graphml xmlns="http://graphml.graphml.org/xmlns"',
691
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
692
+ ' xsi:schemaLocation="http://graphml.graphml.org/xmlns http://graphml.graphml.org/xmlns/1.0/graphml.xsd">',
693
+ ];
694
+ for (const kd of keyDefs) {
695
+ lines.push(` <key id="${kd.id}" for="${kd.for_}" attr.name="${escapeXml(kd.name)}" attr.type="${kd.type}" />`);
696
+ }
697
+ lines.push(' <graph id="G" edgedefault="directed">');
698
+ // Nodes.
699
+ for (const [id, attrs] of G.nodesWithData()) {
700
+ lines.push(` <node id="${escapeXml(id)}">`);
701
+ for (const [k, v] of Object.entries(attrs)) {
702
+ const kid = keyMap.get(`node:${k}`);
703
+ if (kid != null && v != null) {
704
+ const sv = typeof v === "object" ? JSON.stringify(v) : String(v);
705
+ lines.push(` <data key="${kid}">${escapeXml(sv)}</data>`);
706
+ }
707
+ }
708
+ lines.push(" </node>");
709
+ }
710
+ // Edges.
711
+ let edgeIdx = 0;
712
+ for (const [src, tgt, attrs] of G.edgesWithData()) {
713
+ lines.push(` <edge id="e${edgeIdx++}" source="${escapeXml(src)}" target="${escapeXml(tgt)}">`);
714
+ for (const [k, v] of Object.entries(attrs)) {
715
+ const kid = keyMap.get(`edge:${k}`);
716
+ if (kid != null && v != null) {
717
+ const sv = typeof v === "object" ? JSON.stringify(v) : String(v);
718
+ lines.push(` <data key="${kid}">${escapeXml(sv)}</data>`);
719
+ }
720
+ }
721
+ lines.push(" </edge>");
722
+ }
723
+ lines.push(" </graph>");
724
+ lines.push("</graphml>");
725
+ return lines.join("\n");
726
+ }
727
+ /** Map of export format names to their exporter functions. */
728
+ export const EXPORTERS = {
729
+ json: exportJson,
730
+ graphml: exportGraphml,
731
+ };
732
+ //# sourceMappingURL=insider-graph.js.map