@odla-ai/kg 0.3.0 → 0.4.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.
@@ -1,6 +1,6 @@
1
- import { h as GraphNode, G as GraphFragment, a as SourceEntry } from '../sources-nJyD0JUP.js';
2
- import { Value } from '@odla-ai/db';
3
- import '@odla-ai/ai';
1
+ import { b as GraphNode, G as GraphFragment } from '../lenses-C7Q7Gvnx.js';
2
+ export { W as WeightedPair, X as XY, d as blendPairs, f as composeTargets, g as directTargets, h as geoPairs, p as projectBridges, r as reifiedBridgeTargets, t as timePairs } from '../lenses-C7Q7Gvnx.js';
3
+ import '@odla-ai/db';
4
4
 
5
5
  /**
6
6
  * FNV-1a 32-bit hash of a string. Used to derive a per-identity PRNG seed
@@ -76,209 +76,6 @@ declare function indexGraph(fragment: GraphFragment): GraphIndex;
76
76
  */
77
77
  declare function resolvePath(g: GraphIndex, start: NodeRef, labels: readonly string[]): NodeRef[];
78
78
 
79
- /**
80
- * Where an event's geography lives: walk `path` (forward edge labels) from the
81
- * event node, then read `attr` off the landing node — or, with no `attr`, use
82
- * the landing node's key (e.g. a `region` entity slug). With no `path`, the
83
- * event node itself is the landing node.
84
- */
85
- interface GeoSpec {
86
- path?: readonly string[];
87
- attr?: string;
88
- }
89
- /** How to turn one entity type into temporal events. */
90
- interface EventSpec {
91
- /** Event entity type, e.g. `"funding_round"` or `"signal"`. */
92
- type: string;
93
- /** Attr holding the event date (parsed with `parseDateMs`; undated nodes are skipped). */
94
- dateAttr: string;
95
- /** Attr holding a magnitude, parsed with `parseAmount`; unparseable → weight 1. */
96
- weightAttr?: string;
97
- /** Override hook for weights the attr grammar can't express. */
98
- weightOf?: (attrs: Record<string, Value>) => number | undefined;
99
- /** Edge-label paths from the event node to its participant entities. */
100
- attach?: readonly (readonly string[])[];
101
- /** Geography of the event (choropleth/hotspot keying). */
102
- geo?: GeoSpec;
103
- /** Destination geography — only for flow-shaped events (money moving A→B). */
104
- geoTo?: GeoSpec;
105
- }
106
- /** The extraction contract: one spec per event entity type. */
107
- interface TemporalConfig {
108
- events: EventSpec[];
109
- }
110
- /** One timestamped event extracted from the graph. */
111
- interface TemporalEvent {
112
- /** Epoch ms (UTC). */
113
- t: number;
114
- /** The spec's entity type. */
115
- kind: string;
116
- /** The event node itself. */
117
- node: NodeRef;
118
- /** Attached participant entities, deduped, sorted by id. */
119
- refs: NodeRef[];
120
- /** Magnitude (defaults to 1). */
121
- weight: number;
122
- /** Source geography key, when resolvable. */
123
- geo?: string;
124
- /** Destination geography key, when resolvable (flow events). */
125
- geoTo?: string;
126
- }
127
- /**
128
- * Parse a human money/quantity string to a number: `"$12.5M"` → 12500000,
129
- * `"12,500,000"` → 12500000, `"500k"` → 500000, numbers pass through.
130
- * Returns undefined for anything else (never NaN).
131
- */
132
- declare function parseAmount(v: unknown): number | undefined;
133
- /**
134
- * Extract the event stream: for every spec, every node of that type with a
135
- * parseable date becomes one event. Sorted by time, tie-broken by node id —
136
- * the ordering is total and reproducible.
137
- */
138
- declare function extractEvents(fragment: GraphFragment, config: TemporalConfig): TemporalEvent[];
139
-
140
- /** One named series, index-aligned with its SeriesSet's buckets. */
141
- interface Series {
142
- key: string;
143
- values: number[];
144
- }
145
- /** A grain, its buckets, and every series over them (sorted by key). */
146
- interface SeriesSet {
147
- grain: TimeGrain;
148
- buckets: TimeBucket[];
149
- series: Series[];
150
- }
151
- /**
152
- * How to key events into series: `"kind"` (event type), `"geo"` (source
153
- * geography; geo-less events are dropped), `"ref"` (fan one event into one
154
- * increment per attached participant, keyed by node id), or a custom function
155
- * returning zero or more keys per event.
156
- */
157
- type SeriesKeyer = "kind" | "geo" | "ref" | ((e: TemporalEvent) => string | readonly string[] | undefined);
158
- /** The series keys one event contributes to under a keyer (used by rollup and by hotspot contributor attachment). */
159
- declare function seriesKeys(e: TemporalEvent, by: SeriesKeyer): readonly string[];
160
- /**
161
- * Roll events up into bucketed series. The bucket range spans `from`..`to`
162
- * when given (zero-padded even where nothing happened — burst baselines need
163
- * the quiet buckets), else the events' own time span. Events outside the
164
- * range are ignored.
165
- */
166
- declare function rollup(events: readonly TemporalEvent[], opts: {
167
- grain: TimeGrain;
168
- by?: SeriesKeyer;
169
- measure?: "count" | "weight";
170
- from?: number;
171
- to?: number;
172
- }): SeriesSet;
173
- /** Per-bucket sum across every series in the set (the "all" line). */
174
- declare function totals(set: SeriesSet): Series;
175
-
176
- /** Tuning for {@link burstScores}/{@link detectBursts}. */
177
- interface BurstOptions {
178
- /** Trailing buckets forming the baseline (excluding the scored bucket). Default 6. */
179
- window?: number;
180
- /** z needed to OPEN a burst run. Default 2. */
181
- zEnter?: number;
182
- /** z needed to EXTEND an open run (hysteresis). Default 1. */
183
- zExit?: number;
184
- /** Minimum trailing buckets before scoring (cold start → z 0). Default 3. */
185
- minBaseline?: number;
186
- /**
187
- * Floor on the baseline sigma. sqrt(mean) is always applied (Poisson-
188
- * consistent for counts); this guards the all-zero baseline. Default 1.
189
- */
190
- minSigma?: number;
191
- }
192
- /**
193
- * Per-bucket burst z-scores for one series: (value − trailing mean) /
194
- * max(trailing stdev, sqrt(mean), minSigma). Cold-start buckets score 0.
195
- */
196
- declare function burstScores(values: readonly number[], opts?: BurstOptions): number[];
197
- /** One detected burst: a maximal hysteresis run within one series. */
198
- interface Burst {
199
- seriesKey: string;
200
- /** Bucket indexes, inclusive. */
201
- start: number;
202
- end: number;
203
- /** Highest z in the run. */
204
- peak: number;
205
- /** Σ max(0, z) over the run — rewards both height and duration. */
206
- score: number;
207
- }
208
- /**
209
- * All bursts across a SeriesSet, ranked score desc (ties: earlier start,
210
- * then series key). A run opens at z ≥ zEnter and extends while z ≥ zExit.
211
- */
212
- declare function detectBursts(set: SeriesSet, opts?: BurstOptions): Burst[];
213
- /** A burst plus its bucket keys and top contributing events — a ranked lead. */
214
- interface Hotspot extends Burst {
215
- bucketKeys: string[];
216
- contributors: TemporalEvent[];
217
- }
218
- /**
219
- * Detect, rank, and enrich bursts into explorable leads. `by` must be the
220
- * same keyer the SeriesSet was rolled up with — it re-derives each event's
221
- * series keys to attach contributors (top per-lead by weight, then time).
222
- */
223
- declare function rankHotspots(set: SeriesSet, events: readonly TemporalEvent[], opts?: BurstOptions & {
224
- by?: SeriesKeyer;
225
- top?: number;
226
- contributorsPer?: number;
227
- }): Hotspot[];
228
-
229
- /** One dated undirected edge (canonical order: a < b). */
230
- interface TimelineEdge {
231
- a: NodeId;
232
- b: NodeId;
233
- t: number;
234
- kind: string;
235
- weight: number;
236
- }
237
- /** The replayable structure built once from the event stream. */
238
- interface Timeline {
239
- events: TemporalEvent[];
240
- /** Every node ever seen, sorted by id. */
241
- nodes: NodeId[];
242
- /** Dated edges, time-ordered. */
243
- edges: TimelineEdge[];
244
- /** Node id → first time it appeared. */
245
- firstSeen: Map<NodeId, number>;
246
- }
247
- /**
248
- * Build the timeline. `"entities"` (default) projects each event onto its
249
- * participants — every ref pair co-occurring in one event becomes an edge
250
- * (companies↔investors↔people), and event nodes vanish. `"full"` keeps the
251
- * event node and links it to each ref (bipartite).
252
- */
253
- declare function buildTimeline(events: readonly TemporalEvent[], opts?: {
254
- mode?: "entities" | "full";
255
- }): Timeline;
256
- /** The graph as it stood at time `t`. */
257
- interface Snapshot {
258
- t: number;
259
- /** Nodes first seen at or before t, sorted. */
260
- nodes: NodeId[];
261
- /** Aggregated undirected edges (weight = co-occurrence count). */
262
- edges: Array<{
263
- a: NodeId;
264
- b: NodeId;
265
- weight: number;
266
- }>;
267
- /** Node id → its latest event time ≤ t (drives recency glow). */
268
- activity: Map<NodeId, number>;
269
- }
270
- /**
271
- * Materialize the graph at `t`: cumulative by default, or only edges within
272
- * the trailing `window` ms when given (rolling co-activity).
273
- */
274
- declare function snapshotAt(timeline: Timeline, t: number, opts?: {
275
- window?: number;
276
- }): Snapshot;
277
- /** One snapshot per bucket, each taken at the bucket's inclusive end. */
278
- declare function snapshotSeries(timeline: Timeline, buckets: readonly TimeBucket[], opts?: {
279
- window?: number;
280
- }): Snapshot[];
281
-
282
79
  /**
283
80
  * Asynchronous label propagation with deterministic (seeded) tie-breaking:
284
81
  * - labels start as each node's own id;
@@ -302,252 +99,31 @@ declare function labelPropagation(nodes: readonly NodeId[], edges: ReadonlyArray
302
99
  maxRounds?: number;
303
100
  }): Map<NodeId, string>;
304
101
 
305
- /** One cluster in one frame (members sorted). */
306
- interface Cluster {
307
- id: string;
308
- members: NodeId[];
309
- }
310
- /** All clusters at one instant, id-sorted. */
311
- interface ClusterFrame {
312
- t: number;
313
- clusters: Cluster[];
314
- }
315
- /** A structural change between consecutive frames. */
316
- type ClusterChange = {
317
- kind: "birth";
318
- t: number;
319
- id: string;
320
- } | {
321
- kind: "death";
322
- t: number;
323
- id: string;
324
- } | {
325
- kind: "merge";
326
- t: number;
327
- into: string;
328
- from: string[];
329
- } | {
330
- kind: "split";
331
- t: number;
332
- from: string;
333
- into: string[];
334
- };
335
- /**
336
- * Thread stable cluster ids through per-frame label maps. A current cluster
337
- * inherits the id of its best-Jaccard predecessor at ≥ minJaccard (ties:
338
- * more shared members, then smaller id); when several current clusters claim
339
- * one predecessor the best keeps the id and the rest record a split; a
340
- * current cluster absorbing ≥2 predecessors records a merge. Unmatched
341
- * current clusters are births (id = "c:" + smallest member); vanished ids
342
- * are deaths.
343
- */
344
- declare function trackClusters(frames: ReadonlyArray<{
345
- t: number;
346
- labels: Map<NodeId, string>;
347
- }>, opts?: {
348
- minJaccard?: number;
349
- }): {
350
- frames: ClusterFrame[];
351
- changes: ClusterChange[];
352
- };
353
-
354
- /** A unit-square position (scale to pixels at draw time). */
355
- interface XY {
356
- x: number;
357
- y: number;
358
- }
359
- /** Tuning for {@link forceLayout}. */
360
- interface LayoutOptions {
361
- /** Force iterations. Default 300 (cold); use fewer for warm refinement. */
362
- iterations?: number;
363
- /** Seed mixed into every node's hash-derived initial position. Default 1. */
364
- seed?: number;
365
- /**
366
- * Repulsion multiplier. Default 0.1 — measured on a real ~800-node OSINT
367
- * graph as the point where nothing pins to the clamped boundary.
368
- */
369
- repulsion?: number;
370
- /** Pull toward the center, proportional to distance. Default 0.5. */
371
- gravity?: number;
372
- /** Warm-start positions (nodes present here start where they were). */
373
- positions?: ReadonlyMap<NodeId, XY>;
102
+ /** One node's brokerage: normalized score + the communities it connects. */
103
+ interface Bridging {
104
+ /** 0..1, 1 = the frame's strongest connector. */
105
+ score: number;
106
+ /** Communities this node touches through its edges (own community first). */
107
+ groups: string[];
374
108
  }
375
109
  /**
376
- * Lay out one graph. Init priority per node: warm `positions` entry →
377
- * hash-disc from (id, seed). Forces per iteration (accumulated over
378
- * sorted-id order, applied synchronously): pairwise repulsion k²/d, edge
379
- * attraction d²/k scaled by min(weight, 4), center gravity. Linear cooling
380
- * caps displacement; results clamp to [0,1]².
110
+ * Score every node's brokerage against a community partition. Edges inside a
111
+ * community contribute nothing; an edge to a foreign community contributes
112
+ * its weight, and the total is multiplied by the number of DISTINCT foreign
113
+ * communities — a node wiring three groups together outranks one with the
114
+ * same raw weight into a single neighbor group. Scores are normalized to
115
+ * the frame's max (empty/one-community frames → all zeros).
381
116
  */
382
- declare function forceLayout(nodes: readonly NodeId[], edges: ReadonlyArray<{
117
+ declare function bridgingScores(edges: ReadonlyArray<{
383
118
  a: NodeId;
384
119
  b: NodeId;
385
120
  weight?: number;
386
- }>, opts?: LayoutOptions): Map<NodeId, XY>;
387
- /**
388
- * Lay out a snapshot sequence: frame 0 runs cold (`iterations`), each later
389
- * frame warm-starts from the previous frame's positions and refines for
390
- * `warmIterations` — existing nodes drift instead of re-scrambling, so
391
- * inter-frame motion means something.
392
- */
393
- declare function layoutSequence(snapshots: readonly Snapshot[], opts?: LayoutOptions & {
394
- warmIterations?: number;
395
- }): Map<NodeId, XY>[];
396
-
397
- /** Tuning for {@link buildTemporalReport}. */
398
- interface ReportOptions {
399
- /** Analysis anchor (epoch ms). Default: the latest event time. Inject for determinism. */
400
- now?: number;
401
- /** Weekly cluster/lead window. Default 155 days (~22 frames). */
402
- windowDays?: number;
403
- /** Monthly density-series span. Default 12 months. */
404
- monthlyMonths?: number;
405
- /** Ranked leads kept. Default 8. */
406
- topLeads?: number;
407
- /** Contributing events per lead. Default 3. */
408
- contributorsPer?: number;
409
- /** Trailing weeks of cluster changes kept. Default 4. */
410
- changeWeeks?: number;
411
- /** Burst detector tuning. Default `{ window: 6 }`. */
412
- burst?: BurstOptions;
413
- }
414
- /** One ranked hotspot lead: a bursting entity plus its evidence. */
415
- interface ReportLead {
416
- key: string;
417
- type: string;
418
- name: string;
419
- city?: string;
420
- sector?: string;
121
+ }>, labels: Map<NodeId, string>): Map<NodeId, Bridging>;
122
+ /** The frame's strongest connectors, ranked (ties: id order). */
123
+ declare function topBridges(bridging: Map<NodeId, Bridging>, n?: number): Array<{
124
+ id: NodeId;
421
125
  score: number;
422
- peak: number;
423
- window: string[];
424
- contributors: Array<{
425
- kind: string;
426
- key: string;
427
- title: string;
428
- date: string;
429
- }>;
430
- }
431
- /** One cluster-evolution change, name-resolved for display. */
432
- interface ReportClusterChange {
433
- week: string;
434
- kind: "birth" | "death" | "merge" | "split";
435
- ids: string[];
436
- names: string[];
437
- }
438
- /** The cached/served analysis product — everything JSON-native and bounded (≲20KB). */
439
- interface TemporalReport {
440
- generatedAt: string;
441
- window: {
442
- from: string;
443
- to: string;
444
- weeks: string[];
445
- };
446
- totals: {
447
- events: number;
448
- nodes: number;
449
- edges: number;
450
- };
451
- series: Array<{
452
- kind: string;
453
- months: string[];
454
- values: number[];
455
- }>;
456
- leads: ReportLead[];
457
- clusters: {
458
- frames: Array<{
459
- week: string;
460
- count: number;
461
- top: Array<{
462
- id: string;
463
- size: number;
464
- label: string;
465
- }>;
466
- }>;
467
- changes: ReportClusterChange[];
468
- };
469
- }
470
- /** Build the full temporal report for a graph fragment. Empty input → zeroed report. */
471
- declare function buildTemporalReport(fragment: GraphFragment, config: TemporalConfig, opts?: ReportOptions): TemporalReport;
472
-
473
- /** True when the report has anything worth narrating (any lead or recent cluster change). */
474
- declare function shouldNarrate(report: TemporalReport): boolean;
475
- /**
476
- * The deterministic materials block handed to the narrative LLM call:
477
- * window/totals header, top leads with evidence, cluster changes, and
478
- * per-kind monthly density lines. No prose, no interpretation.
479
- */
480
- declare function buildNarrationMaterials(report: TemporalReport, opts?: {
481
- topLeads?: number;
482
- }): string;
483
- /** First-line `# title` heuristic shared with the digest workflow. */
484
- declare function splitTitleBody(text: string, fallbackTitle: string): {
485
- title: string;
486
- body: string;
487
- };
488
-
489
- /**
490
- * Burst score per natural key for one entity type, from a report's leads:
491
- * `{ leads: [{key: "company:acme", score: 7.3}] }` → `{ acme: 7.3 }`.
492
- */
493
- declare function leadScores(report: Pick<TemporalReport, "leads">, type?: string): Record<string, number>;
494
- /** One selected enrichment target. */
495
- interface EnrichmentCandidate {
496
- slug: string;
497
- name: string;
498
- score: number;
499
- }
500
- /**
501
- * Rank enrichment candidates: burst score descending, ties keep the input
502
- * (creation) order. Cooling rows are skipped at selection. Complete rows are
503
- * skipped UNLESS they carry a burst score — a bursting complete company
504
- * deserves a re-check. With no scores this reproduces the legacy behavior
505
- * exactly (input order, incomplete rows only).
506
- */
507
- declare function rankEnrichmentCandidates(rows: ReadonlyArray<Record<string, unknown>>, opts: {
508
- limit: number;
509
- scores?: Record<string, number>;
510
- cooling?: ReadonlySet<string>;
511
- isComplete?: (row: Record<string, unknown>) => boolean;
512
- }): EnrichmentCandidate[];
513
- /**
514
- * Replace the synthesized burst-sweep source with a fresh one derived from
515
- * the report's top company leads (or remove it when there are none).
516
- * Hand-authored entries — anything without `origin: "burst"` — are returned
517
- * untouched, in order. Queries follow
518
- * `"<name>" <city|sector|startup> funding OR expansion OR hiring news`.
519
- */
520
- declare function synthesizeBurstSources(existing: readonly SourceEntry[], report: Pick<TemporalReport, "leads">, opts?: {
521
- max?: number;
522
- cadenceHours?: number;
523
- }): SourceEntry[];
524
-
525
- /** One aggregated movement between two geography keys within a bucket. */
526
- interface Flow {
527
- from: string;
528
- to: string;
529
- /** Summed event weights (e.g. dollars). */
530
- weight: number;
531
- /** Number of contributing events. */
532
- count: number;
533
- }
534
- /** All flows for one time bucket, sorted weight-desc then from/to. */
535
- interface FlowFrame {
536
- bucket: TimeBucket;
537
- flows: Flow[];
538
- }
539
- /**
540
- * Aggregate flow events into per-bucket frames over `from`..`to` (default:
541
- * the events' span). With `cumulative`, each frame includes everything up to
542
- * and including its bucket — replay-style accumulation.
543
- */
544
- declare function flowFrames(events: readonly TemporalEvent[], opts: {
545
- grain: TimeGrain;
546
- from?: number;
547
- to?: number;
548
- cumulative?: boolean;
549
- }): FlowFrame[];
550
- /** The frame's top-n flows by weight (already the frame's sort order). */
551
- declare function topFlows(frame: FlowFrame, n: number): Flow[];
126
+ groups: string[];
127
+ }>;
552
128
 
553
- export { type Burst, type BurstOptions, type Cluster, type ClusterChange, type ClusterFrame, type EnrichmentCandidate, type EventSpec, type Flow, type FlowFrame, type GeoSpec, type GraphIndex, type Hotspot, type LayoutOptions, type NodeId, type NodeRef, type ReportClusterChange, type ReportLead, type ReportOptions, type Series, type SeriesKeyer, type SeriesSet, type Snapshot, type TemporalConfig, type TemporalEvent, type TemporalReport, type TimeBucket, type TimeGrain, type Timeline, type TimelineEdge, type XY, bucketFor, bucketKey, bucketRange, buildNarrationMaterials, buildTemporalReport, buildTimeline, burstScores, byNodeId, detectBursts, extractEvents, flowFrames, forceLayout, hash32, indexGraph, labelPropagation, layoutSequence, leadScores, mulberry32, nodeId, parseAmount, parseDateMs, rankEnrichmentCandidates, rankHotspots, resolvePath, rollup, seriesKeys, shouldNarrate, shuffle, snapshotAt, snapshotSeries, splitTitleBody, synthesizeBurstSources, topFlows, totals, trackClusters };
129
+ export { type Bridging, type GraphIndex, type NodeId, type NodeRef, type TimeBucket, type TimeGrain, bridgingScores, bucketFor, bucketKey, bucketRange, byNodeId, hash32, indexGraph, labelPropagation, mulberry32, nodeId, parseDateMs, resolvePath, shuffle, topBridges };