@odla-ai/kg 0.3.0 → 0.3.2

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.
@@ -550,4 +550,141 @@ declare function flowFrames(events: readonly TemporalEvent[], opts: {
550
550
  /** The frame's top-n flows by weight (already the frame's sort order). */
551
551
  declare function topFlows(frame: FlowFrame, n: number): Flow[];
552
552
 
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 };
553
+ /**
554
+ * One threshold rule over a region's static metrics. A rule matches when the
555
+ * region has the metric and its value passes the comparison; regions or
556
+ * metrics missing from the metric map never match (and never throw).
557
+ */
558
+ interface SpatialRule {
559
+ /** Stable identifier, reported on findings (e.g. `"capital-desert"`). */
560
+ id: string;
561
+ /** Metric name looked up in the region's metric record. */
562
+ metric: string;
563
+ /** Comparison direction against {@link threshold}. */
564
+ op: ">=" | "<=";
565
+ threshold: number;
566
+ /** Additive score boost when matched (finding score × (1 + Σ weights)). Default 1. */
567
+ weight?: number;
568
+ }
569
+ /** A bursting region, the rules it matched, and its boosted rank score. */
570
+ interface SpatialFinding {
571
+ /** The series key — the region identifier the events were rolled up by. */
572
+ region: string;
573
+ /** The region's strongest burst (highest score) in the set. */
574
+ burst: Burst;
575
+ /** Ids of the rules that matched, in the rules' given order. */
576
+ matched: string[];
577
+ /** `burst.score × (1 + Σ matched weights)` — rule context amplifies heat. */
578
+ score: number;
579
+ /** The region's values for every rule metric it has (evidence for narration). */
580
+ metrics: Record<string, number>;
581
+ }
582
+ /** Ids of the rules a region's metric record matches, in rule order. */
583
+ declare function matchRules(values: Record<string, number> | undefined, rules: readonly SpatialRule[]): string[];
584
+ /**
585
+ * Rank spatial anomalies: detect bursts in a geo-keyed SeriesSet (one series
586
+ * per region — roll up with `by: "geo"`), keep each region's strongest burst,
587
+ * then amplify by matched metric rules. Deterministic: score desc, then
588
+ * region asc. `requireMatch` drops regions matching no rule (default keeps
589
+ * them — a burst is a finding even without metric context).
590
+ */
591
+ declare function spatialFindings(set: SeriesSet, metrics: Record<string, Record<string, number> | undefined>, rules: readonly SpatialRule[], opts?: BurstOptions & {
592
+ top?: number;
593
+ requireMatch?: boolean;
594
+ }): SpatialFinding[];
595
+
596
+ /** One undirected weighted pair, with (capped) bridging node ids as evidence. */
597
+ interface WeightedPair {
598
+ a: string;
599
+ b: string;
600
+ w: number;
601
+ via: string[];
602
+ }
603
+ /**
604
+ * Bridge → target sets through a REIFIED relationship node (affiliation,
605
+ * funding_round, record): every reified node with a `bridgeLabel` edge to the
606
+ * bridge and a `targetLabel` edge to a target contributes that target to the
607
+ * bridge's set. Ids are `type:key`.
608
+ */
609
+ declare function reifiedBridgeTargets(fragment: GraphFragment, opts: {
610
+ reified: string;
611
+ bridgeLabel: string;
612
+ targetLabel: string;
613
+ }): Map<string, Set<string>>;
614
+ /** Direct-edge targets: `from` node id → set of `to` node ids for one label. */
615
+ declare function directTargets(fragment: GraphFragment, label: string): Map<string, Set<string>>;
616
+ /** Compose A→B with B→C into A→C (e.g. person→companies ∘ company→counties). */
617
+ declare function composeTargets(ab: Map<string, Set<string>>, bc: Map<string, Set<string>>): Map<string, Set<string>>;
618
+ /**
619
+ * The bipartite projection: targets sharing a bridge become pairs, weight =
620
+ * number of distinct shared bridges. `maxPerBridge` skips promiscuous hubs
621
+ * (a bridge touching 40 targets says little about any pair). Deterministic
622
+ * pair order (a < b, then sorted).
623
+ */
624
+ declare function projectBridges(bridges: Map<string, Set<string>>, opts?: {
625
+ maxPerBridge?: number;
626
+ viaCap?: number;
627
+ }): WeightedPair[];
628
+ /**
629
+ * Temporal cohort pairs: each node links to its k nearest neighbors in
630
+ * arrival time, weight decaying with the gap (0.5 per halfLife). Nodes
631
+ * without a date contribute nothing.
632
+ */
633
+ declare function timePairs(arrivals: Map<string, number>, opts?: {
634
+ k?: number;
635
+ halfLifeDays?: number;
636
+ }): WeightedPair[];
637
+ /**
638
+ * Spatial proximity pairs: k nearest neighbors in normalized map space,
639
+ * weight decaying with distance (0.5 per halfDist). Nodes without a
640
+ * position contribute nothing. O(n²) — fine for the ≤1k-node universes
641
+ * lenses operate on.
642
+ */
643
+ declare function geoPairs(positions: Map<string, XY>, opts?: {
644
+ k?: number;
645
+ halfDist?: number;
646
+ }): WeightedPair[];
647
+ /**
648
+ * Blend lens edge sets: each set is normalized to a max weight of 1 (a lens
649
+ * with big raw counts must not drown the others), scaled by its slider
650
+ * weight, and summed per pair. Zero-weight lenses drop out entirely.
651
+ */
652
+ declare function blendPairs(sets: Array<{
653
+ pairs: WeightedPair[];
654
+ weight: number;
655
+ }>): WeightedPair[];
656
+ /**
657
+ * Fabricate the Snapshot series a lens graph replays: at each bucket end,
658
+ * nodes that have arrived and the blended edges among them. Slots straight
659
+ * into labelPropagation/trackClusters/layoutSequence.
660
+ */
661
+ declare function lensSnapshots(arrivals: Map<string, number>, pairs: WeightedPair[], buckets: readonly TimeBucket[]): Snapshot[];
662
+
663
+ /** One node's brokerage: normalized score + the communities it connects. */
664
+ interface Bridging {
665
+ /** 0..1, 1 = the frame's strongest connector. */
666
+ score: number;
667
+ /** Communities this node touches through its edges (own community first). */
668
+ groups: string[];
669
+ }
670
+ /**
671
+ * Score every node's brokerage against a community partition. Edges inside a
672
+ * community contribute nothing; an edge to a foreign community contributes
673
+ * its weight, and the total is multiplied by the number of DISTINCT foreign
674
+ * communities — a node wiring three groups together outranks one with the
675
+ * same raw weight into a single neighbor group. Scores are normalized to
676
+ * the frame's max (empty/one-community frames → all zeros).
677
+ */
678
+ declare function bridgingScores(edges: ReadonlyArray<{
679
+ a: NodeId;
680
+ b: NodeId;
681
+ weight?: number;
682
+ }>, labels: Map<NodeId, string>): Map<NodeId, Bridging>;
683
+ /** The frame's strongest connectors, ranked (ties: id order). */
684
+ declare function topBridges(bridging: Map<NodeId, Bridging>, n?: number): Array<{
685
+ id: NodeId;
686
+ score: number;
687
+ groups: string[];
688
+ }>;
689
+
690
+ export { type Bridging, 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 SpatialFinding, type SpatialRule, type TemporalConfig, type TemporalEvent, type TemporalReport, type TimeBucket, type TimeGrain, type Timeline, type TimelineEdge, type WeightedPair, type XY, blendPairs, bridgingScores, bucketFor, bucketKey, bucketRange, buildNarrationMaterials, buildTemporalReport, buildTimeline, burstScores, byNodeId, composeTargets, detectBursts, directTargets, extractEvents, flowFrames, forceLayout, geoPairs, hash32, indexGraph, labelPropagation, layoutSequence, leadScores, lensSnapshots, matchRules, mulberry32, nodeId, parseAmount, parseDateMs, projectBridges, rankEnrichmentCandidates, rankHotspots, reifiedBridgeTargets, resolvePath, rollup, seriesKeys, shouldNarrate, shuffle, snapshotAt, snapshotSeries, spatialFindings, splitTitleBody, synthesizeBurstSources, timePairs, topBridges, topFlows, totals, trackClusters };
@@ -550,4 +550,141 @@ declare function flowFrames(events: readonly TemporalEvent[], opts: {
550
550
  /** The frame's top-n flows by weight (already the frame's sort order). */
551
551
  declare function topFlows(frame: FlowFrame, n: number): Flow[];
552
552
 
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 };
553
+ /**
554
+ * One threshold rule over a region's static metrics. A rule matches when the
555
+ * region has the metric and its value passes the comparison; regions or
556
+ * metrics missing from the metric map never match (and never throw).
557
+ */
558
+ interface SpatialRule {
559
+ /** Stable identifier, reported on findings (e.g. `"capital-desert"`). */
560
+ id: string;
561
+ /** Metric name looked up in the region's metric record. */
562
+ metric: string;
563
+ /** Comparison direction against {@link threshold}. */
564
+ op: ">=" | "<=";
565
+ threshold: number;
566
+ /** Additive score boost when matched (finding score × (1 + Σ weights)). Default 1. */
567
+ weight?: number;
568
+ }
569
+ /** A bursting region, the rules it matched, and its boosted rank score. */
570
+ interface SpatialFinding {
571
+ /** The series key — the region identifier the events were rolled up by. */
572
+ region: string;
573
+ /** The region's strongest burst (highest score) in the set. */
574
+ burst: Burst;
575
+ /** Ids of the rules that matched, in the rules' given order. */
576
+ matched: string[];
577
+ /** `burst.score × (1 + Σ matched weights)` — rule context amplifies heat. */
578
+ score: number;
579
+ /** The region's values for every rule metric it has (evidence for narration). */
580
+ metrics: Record<string, number>;
581
+ }
582
+ /** Ids of the rules a region's metric record matches, in rule order. */
583
+ declare function matchRules(values: Record<string, number> | undefined, rules: readonly SpatialRule[]): string[];
584
+ /**
585
+ * Rank spatial anomalies: detect bursts in a geo-keyed SeriesSet (one series
586
+ * per region — roll up with `by: "geo"`), keep each region's strongest burst,
587
+ * then amplify by matched metric rules. Deterministic: score desc, then
588
+ * region asc. `requireMatch` drops regions matching no rule (default keeps
589
+ * them — a burst is a finding even without metric context).
590
+ */
591
+ declare function spatialFindings(set: SeriesSet, metrics: Record<string, Record<string, number> | undefined>, rules: readonly SpatialRule[], opts?: BurstOptions & {
592
+ top?: number;
593
+ requireMatch?: boolean;
594
+ }): SpatialFinding[];
595
+
596
+ /** One undirected weighted pair, with (capped) bridging node ids as evidence. */
597
+ interface WeightedPair {
598
+ a: string;
599
+ b: string;
600
+ w: number;
601
+ via: string[];
602
+ }
603
+ /**
604
+ * Bridge → target sets through a REIFIED relationship node (affiliation,
605
+ * funding_round, record): every reified node with a `bridgeLabel` edge to the
606
+ * bridge and a `targetLabel` edge to a target contributes that target to the
607
+ * bridge's set. Ids are `type:key`.
608
+ */
609
+ declare function reifiedBridgeTargets(fragment: GraphFragment, opts: {
610
+ reified: string;
611
+ bridgeLabel: string;
612
+ targetLabel: string;
613
+ }): Map<string, Set<string>>;
614
+ /** Direct-edge targets: `from` node id → set of `to` node ids for one label. */
615
+ declare function directTargets(fragment: GraphFragment, label: string): Map<string, Set<string>>;
616
+ /** Compose A→B with B→C into A→C (e.g. person→companies ∘ company→counties). */
617
+ declare function composeTargets(ab: Map<string, Set<string>>, bc: Map<string, Set<string>>): Map<string, Set<string>>;
618
+ /**
619
+ * The bipartite projection: targets sharing a bridge become pairs, weight =
620
+ * number of distinct shared bridges. `maxPerBridge` skips promiscuous hubs
621
+ * (a bridge touching 40 targets says little about any pair). Deterministic
622
+ * pair order (a < b, then sorted).
623
+ */
624
+ declare function projectBridges(bridges: Map<string, Set<string>>, opts?: {
625
+ maxPerBridge?: number;
626
+ viaCap?: number;
627
+ }): WeightedPair[];
628
+ /**
629
+ * Temporal cohort pairs: each node links to its k nearest neighbors in
630
+ * arrival time, weight decaying with the gap (0.5 per halfLife). Nodes
631
+ * without a date contribute nothing.
632
+ */
633
+ declare function timePairs(arrivals: Map<string, number>, opts?: {
634
+ k?: number;
635
+ halfLifeDays?: number;
636
+ }): WeightedPair[];
637
+ /**
638
+ * Spatial proximity pairs: k nearest neighbors in normalized map space,
639
+ * weight decaying with distance (0.5 per halfDist). Nodes without a
640
+ * position contribute nothing. O(n²) — fine for the ≤1k-node universes
641
+ * lenses operate on.
642
+ */
643
+ declare function geoPairs(positions: Map<string, XY>, opts?: {
644
+ k?: number;
645
+ halfDist?: number;
646
+ }): WeightedPair[];
647
+ /**
648
+ * Blend lens edge sets: each set is normalized to a max weight of 1 (a lens
649
+ * with big raw counts must not drown the others), scaled by its slider
650
+ * weight, and summed per pair. Zero-weight lenses drop out entirely.
651
+ */
652
+ declare function blendPairs(sets: Array<{
653
+ pairs: WeightedPair[];
654
+ weight: number;
655
+ }>): WeightedPair[];
656
+ /**
657
+ * Fabricate the Snapshot series a lens graph replays: at each bucket end,
658
+ * nodes that have arrived and the blended edges among them. Slots straight
659
+ * into labelPropagation/trackClusters/layoutSequence.
660
+ */
661
+ declare function lensSnapshots(arrivals: Map<string, number>, pairs: WeightedPair[], buckets: readonly TimeBucket[]): Snapshot[];
662
+
663
+ /** One node's brokerage: normalized score + the communities it connects. */
664
+ interface Bridging {
665
+ /** 0..1, 1 = the frame's strongest connector. */
666
+ score: number;
667
+ /** Communities this node touches through its edges (own community first). */
668
+ groups: string[];
669
+ }
670
+ /**
671
+ * Score every node's brokerage against a community partition. Edges inside a
672
+ * community contribute nothing; an edge to a foreign community contributes
673
+ * its weight, and the total is multiplied by the number of DISTINCT foreign
674
+ * communities — a node wiring three groups together outranks one with the
675
+ * same raw weight into a single neighbor group. Scores are normalized to
676
+ * the frame's max (empty/one-community frames → all zeros).
677
+ */
678
+ declare function bridgingScores(edges: ReadonlyArray<{
679
+ a: NodeId;
680
+ b: NodeId;
681
+ weight?: number;
682
+ }>, labels: Map<NodeId, string>): Map<NodeId, Bridging>;
683
+ /** The frame's strongest connectors, ranked (ties: id order). */
684
+ declare function topBridges(bridging: Map<NodeId, Bridging>, n?: number): Array<{
685
+ id: NodeId;
686
+ score: number;
687
+ groups: string[];
688
+ }>;
689
+
690
+ export { type Bridging, 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 SpatialFinding, type SpatialRule, type TemporalConfig, type TemporalEvent, type TemporalReport, type TimeBucket, type TimeGrain, type Timeline, type TimelineEdge, type WeightedPair, type XY, blendPairs, bridgingScores, bucketFor, bucketKey, bucketRange, buildNarrationMaterials, buildTemporalReport, buildTimeline, burstScores, byNodeId, composeTargets, detectBursts, directTargets, extractEvents, flowFrames, forceLayout, geoPairs, hash32, indexGraph, labelPropagation, layoutSequence, leadScores, lensSnapshots, matchRules, mulberry32, nodeId, parseAmount, parseDateMs, projectBridges, rankEnrichmentCandidates, rankHotspots, reifiedBridgeTargets, resolvePath, rollup, seriesKeys, shouldNarrate, shuffle, snapshotAt, snapshotSeries, spatialFindings, splitTitleBody, synthesizeBurstSources, timePairs, topBridges, topFlows, totals, trackClusters };
@@ -759,7 +759,202 @@ function flowFrames(events, opts) {
759
759
  function topFlows(frame, n) {
760
760
  return frame.flows.slice(0, n);
761
761
  }
762
+
763
+ // src/temporal/spatial.ts
764
+ function matchRules(values, rules) {
765
+ if (!values) return [];
766
+ return rules.filter((r) => {
767
+ const v = values[r.metric];
768
+ if (v === void 0) return false;
769
+ return r.op === ">=" ? v >= r.threshold : v <= r.threshold;
770
+ }).map((r) => r.id);
771
+ }
772
+ function spatialFindings(set, metrics, rules, opts = {}) {
773
+ const { top = 20, requireMatch = false } = opts;
774
+ const best = /* @__PURE__ */ new Map();
775
+ for (const burst of detectBursts(set, opts)) {
776
+ if (!best.has(burst.seriesKey)) best.set(burst.seriesKey, burst);
777
+ }
778
+ const findings = [];
779
+ for (const [region, burst] of best) {
780
+ const values = metrics[region];
781
+ const matched = matchRules(values, rules);
782
+ if (requireMatch && matched.length === 0) continue;
783
+ const boost = rules.filter((r) => matched.includes(r.id)).reduce((a, r) => a + (r.weight ?? 1), 0);
784
+ const evidence = {};
785
+ for (const r of rules) {
786
+ const v = values?.[r.metric];
787
+ if (v !== void 0) evidence[r.metric] = v;
788
+ }
789
+ findings.push({ region, burst, matched, score: burst.score * (1 + boost), metrics: evidence });
790
+ }
791
+ return findings.sort((a, b) => b.score - a.score || (a.region < b.region ? -1 : 1)).slice(0, top);
792
+ }
793
+
794
+ // src/temporal/lenses.ts
795
+ function reifiedBridgeTargets(fragment, opts) {
796
+ const bridgesOf = /* @__PURE__ */ new Map();
797
+ const targetsOf = /* @__PURE__ */ new Map();
798
+ for (const e of fragment.edges) {
799
+ if (e.from.type !== opts.reified) continue;
800
+ const to = `${e.to.type}:${e.to.key}`;
801
+ if (e.label === opts.bridgeLabel) (bridgesOf.get(e.from.key) ?? bridgesOf.set(e.from.key, []).get(e.from.key)).push(to);
802
+ if (e.label === opts.targetLabel) (targetsOf.get(e.from.key) ?? targetsOf.set(e.from.key, []).get(e.from.key)).push(to);
803
+ }
804
+ const out = /* @__PURE__ */ new Map();
805
+ for (const [reifiedKey, bridges] of bridgesOf) {
806
+ for (const bridge of bridges) {
807
+ for (const target of targetsOf.get(reifiedKey) ?? []) {
808
+ (out.get(bridge) ?? out.set(bridge, /* @__PURE__ */ new Set()).get(bridge)).add(target);
809
+ }
810
+ }
811
+ }
812
+ return out;
813
+ }
814
+ function directTargets(fragment, label) {
815
+ const out = /* @__PURE__ */ new Map();
816
+ for (const e of fragment.edges) {
817
+ if (e.label !== label) continue;
818
+ const from = `${e.from.type}:${e.from.key}`;
819
+ (out.get(from) ?? out.set(from, /* @__PURE__ */ new Set()).get(from)).add(`${e.to.type}:${e.to.key}`);
820
+ }
821
+ return out;
822
+ }
823
+ function composeTargets(ab, bc) {
824
+ const out = /* @__PURE__ */ new Map();
825
+ for (const [a, bs] of ab) {
826
+ for (const b of bs) {
827
+ for (const c of bc.get(b) ?? []) {
828
+ (out.get(a) ?? out.set(a, /* @__PURE__ */ new Set()).get(a)).add(c);
829
+ }
830
+ }
831
+ }
832
+ return out;
833
+ }
834
+ function projectBridges(bridges, opts = {}) {
835
+ const { maxPerBridge = 12, viaCap = 4 } = opts;
836
+ const acc = /* @__PURE__ */ new Map();
837
+ for (const [bridge, targetSet] of [...bridges.entries()].sort((x, y) => x[0] < y[0] ? -1 : 1)) {
838
+ const targets = [...targetSet].sort();
839
+ if (targets.length < 2 || targets.length > maxPerBridge) continue;
840
+ for (let i = 0; i < targets.length; i++) {
841
+ for (let j = i + 1; j < targets.length; j++) {
842
+ const key = `${targets[i]}|${targets[j]}`;
843
+ const hit = acc.get(key) ?? { a: targets[i], b: targets[j], w: 0, via: [] };
844
+ hit.w += 1;
845
+ if (hit.via.length < viaCap) hit.via.push(bridge);
846
+ acc.set(key, hit);
847
+ }
848
+ }
849
+ }
850
+ return [...acc.keys()].sort().map((k) => acc.get(k));
851
+ }
852
+ function timePairs(arrivals, opts = {}) {
853
+ const { k = 4, halfLifeDays = 30 } = opts;
854
+ const halfLife = halfLifeDays * 864e5;
855
+ const rows = [...arrivals.entries()].filter(([, t]) => Number.isFinite(t)).sort((x, y) => x[1] - y[1] || (x[0] < y[0] ? -1 : 1));
856
+ const acc = /* @__PURE__ */ new Map();
857
+ for (let i = 0; i < rows.length; i++) {
858
+ for (let j = i + 1; j < Math.min(rows.length, i + 1 + k); j++) {
859
+ const [idA, tA] = rows[i];
860
+ const [idB, tB] = rows[j];
861
+ const w = Math.pow(0.5, Math.abs(tB - tA) / halfLife);
862
+ if (w < 0.01) break;
863
+ const [a, b] = idA < idB ? [idA, idB] : [idB, idA];
864
+ acc.set(`${a}|${b}`, { a, b, w, via: [] });
865
+ }
866
+ }
867
+ return [...acc.keys()].sort().map((key) => acc.get(key));
868
+ }
869
+ function geoPairs(positions, opts = {}) {
870
+ const { k = 4, halfDist = 0.04 } = opts;
871
+ const rows = [...positions.entries()].sort((x, y) => x[0] < y[0] ? -1 : 1);
872
+ const acc = /* @__PURE__ */ new Map();
873
+ for (let i = 0; i < rows.length; i++) {
874
+ const dists = [];
875
+ for (let j = 0; j < rows.length; j++) {
876
+ if (i === j) continue;
877
+ const dx = rows[i][1].x - rows[j][1].x;
878
+ const dy = rows[i][1].y - rows[j][1].y;
879
+ dists.push({ j, d: Math.hypot(dx, dy) });
880
+ }
881
+ dists.sort((x, y) => x.d - y.d || x.j - y.j);
882
+ for (const { j, d } of dists.slice(0, k)) {
883
+ const w = Math.pow(0.5, d / halfDist);
884
+ if (w < 0.01) continue;
885
+ const [a, b] = rows[i][0] < rows[j][0] ? [rows[i][0], rows[j][0]] : [rows[j][0], rows[i][0]];
886
+ const key = `${a}|${b}`;
887
+ const prev = acc.get(key);
888
+ if (!prev || prev.w < w) acc.set(key, { a, b, w, via: [] });
889
+ }
890
+ }
891
+ return [...acc.keys()].sort().map((key) => acc.get(key));
892
+ }
893
+ function blendPairs(sets) {
894
+ const acc = /* @__PURE__ */ new Map();
895
+ for (const { pairs, weight } of sets) {
896
+ if (weight <= 0 || pairs.length === 0) continue;
897
+ const max = Math.max(...pairs.map((p) => p.w));
898
+ for (const p of pairs) {
899
+ const key = `${p.a}|${p.b}`;
900
+ const hit = acc.get(key) ?? { a: p.a, b: p.b, w: 0, via: [] };
901
+ hit.w += p.w / max * weight;
902
+ for (const v of p.via) if (hit.via.length < 6 && !hit.via.includes(v)) hit.via.push(v);
903
+ acc.set(key, hit);
904
+ }
905
+ }
906
+ return [...acc.keys()].sort().map((key) => acc.get(key));
907
+ }
908
+ function lensSnapshots(arrivals, pairs, buckets) {
909
+ const ids = [...arrivals.keys()].sort();
910
+ return buckets.map((bucket) => {
911
+ const t = bucket.end - 1;
912
+ const nodes = ids.filter((id) => arrivals.get(id) <= t);
913
+ const active = new Set(nodes);
914
+ const edges = pairs.filter((p) => active.has(p.a) && active.has(p.b)).map((p) => ({ a: p.a, b: p.b, weight: p.w }));
915
+ const activity = new Map(nodes.map((id) => [id, arrivals.get(id)]));
916
+ return { t, nodes, edges, activity };
917
+ });
918
+ }
919
+
920
+ // src/temporal/bridges.ts
921
+ function bridgingScores(edges, labels) {
922
+ const cross = /* @__PURE__ */ new Map();
923
+ const foreign = /* @__PURE__ */ new Map();
924
+ const touch = (id, otherLabel, w) => {
925
+ cross.set(id, (cross.get(id) ?? 0) + w);
926
+ (foreign.get(id) ?? foreign.set(id, /* @__PURE__ */ new Set()).get(id)).add(otherLabel);
927
+ };
928
+ for (const e of edges) {
929
+ const la = labels.get(e.a);
930
+ const lb = labels.get(e.b);
931
+ if (la === void 0 || lb === void 0 || la === lb) continue;
932
+ const w = e.weight ?? 1;
933
+ touch(e.a, lb, w);
934
+ touch(e.b, la, w);
935
+ }
936
+ let max = 0;
937
+ const raw = /* @__PURE__ */ new Map();
938
+ for (const [id, w] of cross) {
939
+ const r = w * foreign.get(id).size;
940
+ raw.set(id, r);
941
+ if (r > max) max = r;
942
+ }
943
+ const out = /* @__PURE__ */ new Map();
944
+ for (const [id] of labels) {
945
+ const r = raw.get(id) ?? 0;
946
+ const own = labels.get(id);
947
+ const groups = r > 0 ? [own, ...[...foreign.get(id)].sort()] : [own];
948
+ out.set(id, { score: max > 0 ? r / max : 0, groups });
949
+ }
950
+ return out;
951
+ }
952
+ function topBridges(bridging, n = 5) {
953
+ return [...bridging.entries()].filter(([, b]) => b.score > 0).sort((x, y) => y[1].score - x[1].score || (x[0] < y[0] ? -1 : 1)).slice(0, n).map(([id, b]) => ({ id, score: Math.round(b.score * 100) / 100, groups: b.groups }));
954
+ }
762
955
  export {
956
+ blendPairs,
957
+ bridgingScores,
763
958
  bucketFor,
764
959
  bucketKey,
765
960
  bucketRange,
@@ -768,21 +963,28 @@ export {
768
963
  buildTimeline,
769
964
  burstScores,
770
965
  byNodeId,
966
+ composeTargets,
771
967
  detectBursts,
968
+ directTargets,
772
969
  extractEvents,
773
970
  flowFrames,
774
971
  forceLayout,
972
+ geoPairs,
775
973
  hash32,
776
974
  indexGraph,
777
975
  labelPropagation,
778
976
  layoutSequence,
779
977
  leadScores,
978
+ lensSnapshots,
979
+ matchRules,
780
980
  mulberry32,
781
981
  nodeId,
782
982
  parseAmount,
783
983
  parseDateMs,
984
+ projectBridges,
784
985
  rankEnrichmentCandidates,
785
986
  rankHotspots,
987
+ reifiedBridgeTargets,
786
988
  resolvePath,
787
989
  rollup,
788
990
  seriesKeys,
@@ -790,8 +992,11 @@ export {
790
992
  shuffle,
791
993
  snapshotAt,
792
994
  snapshotSeries,
995
+ spatialFindings,
793
996
  splitTitleBody,
794
997
  synthesizeBurstSources,
998
+ timePairs,
999
+ topBridges,
795
1000
  topFlows,
796
1001
  totals,
797
1002
  trackClusters