@remnic/core 9.3.601 → 9.3.602

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 (47) hide show
  1. package/dist/access-cli.js +6 -6
  2. package/dist/access-http.js +6 -6
  3. package/dist/access-mcp.js +5 -5
  4. package/dist/access-service.js +4 -4
  5. package/dist/causal-behavior.js +2 -2
  6. package/dist/causal-chain.js +2 -2
  7. package/dist/causal-consolidation.js +2 -2
  8. package/dist/causal-retrieval.js +2 -2
  9. package/dist/causal-trajectory-graph.js +1 -1
  10. package/dist/causal-trajectory.js +1 -1
  11. package/dist/{chunk-ELKI4BB6.js → chunk-2ESBDLT5.js} +3 -3
  12. package/dist/{chunk-WZA5Y6AC.js → chunk-2QANQKSQ.js} +3 -3
  13. package/dist/{chunk-BDCCWRHR.js → chunk-5RPTH6AU.js} +3 -3
  14. package/dist/{chunk-JKV57BTN.js → chunk-7V2SGZ3H.js} +2 -2
  15. package/dist/{chunk-D4KJ74JJ.js → chunk-EWC6W6AB.js} +2 -2
  16. package/dist/{chunk-V67GWXM2.js → chunk-IP73YCZP.js} +1 -1
  17. package/dist/{chunk-KDUVQU6Y.js → chunk-MTGOAU7A.js} +4 -4
  18. package/dist/{chunk-65JSA4MP.js → chunk-RUYYYLDT.js} +7 -7
  19. package/dist/{chunk-CL3MWNNQ.js → chunk-TH67Q46T.js} +3 -3
  20. package/dist/{chunk-ZZYF3BUL.js → chunk-TQOU3VAT.js} +1 -1
  21. package/dist/{chunk-4JRRISLU.js → chunk-XL7FK7PJ.js} +61 -43
  22. package/dist/chunk-XL7FK7PJ.js.map +1 -0
  23. package/dist/cli.js +8 -8
  24. package/dist/compounding/engine.js +1 -1
  25. package/dist/{graph-edge-decay-MUP5J7CC.js → graph-edge-decay-5ZOK7ZNO.js} +2 -2
  26. package/dist/graph-snapshot.js +2 -2
  27. package/dist/graph.js +1 -1
  28. package/dist/index.js +10 -10
  29. package/dist/operator-toolkit.js +2 -2
  30. package/dist/orchestrator.js +4 -4
  31. package/dist/schemas.d.ts +22 -22
  32. package/dist/transfer/types.d.ts +12 -12
  33. package/package.json +1 -1
  34. package/src/graph.test.ts +76 -11
  35. package/src/graph.ts +101 -88
  36. package/dist/chunk-4JRRISLU.js.map +0 -1
  37. /package/dist/{chunk-ELKI4BB6.js.map → chunk-2ESBDLT5.js.map} +0 -0
  38. /package/dist/{chunk-WZA5Y6AC.js.map → chunk-2QANQKSQ.js.map} +0 -0
  39. /package/dist/{chunk-BDCCWRHR.js.map → chunk-5RPTH6AU.js.map} +0 -0
  40. /package/dist/{chunk-JKV57BTN.js.map → chunk-7V2SGZ3H.js.map} +0 -0
  41. /package/dist/{chunk-D4KJ74JJ.js.map → chunk-EWC6W6AB.js.map} +0 -0
  42. /package/dist/{chunk-V67GWXM2.js.map → chunk-IP73YCZP.js.map} +0 -0
  43. /package/dist/{chunk-KDUVQU6Y.js.map → chunk-MTGOAU7A.js.map} +0 -0
  44. /package/dist/{chunk-65JSA4MP.js.map → chunk-RUYYYLDT.js.map} +0 -0
  45. /package/dist/{chunk-CL3MWNNQ.js.map → chunk-TH67Q46T.js.map} +0 -0
  46. /package/dist/{chunk-ZZYF3BUL.js.map → chunk-TQOU3VAT.js.map} +0 -0
  47. /package/dist/{graph-edge-decay-MUP5J7CC.js.map → graph-edge-decay-5ZOK7ZNO.js.map} +0 -0
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
- schemaVersion: string;
317
316
  includes: {
318
317
  procedural: boolean;
319
318
  taxonomy: boolean;
320
319
  identityAnchors: boolean;
321
320
  peerProfiles: boolean;
322
321
  };
322
+ schemaVersion: string;
323
323
  id: string;
324
324
  description: string;
325
325
  version: string;
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
- schemaVersion: string;
338
337
  includes: {
339
338
  procedural: boolean;
340
339
  taxonomy: boolean;
341
340
  identityAnchors: boolean;
342
341
  peerProfiles: boolean;
343
342
  };
343
+ schemaVersion: string;
344
344
  id: string;
345
345
  description: string;
346
346
  version: string;
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
- schemaVersion: string;
468
467
  includes: {
469
468
  procedural: boolean;
470
469
  taxonomy: boolean;
471
470
  identityAnchors: boolean;
472
471
  peerProfiles: boolean;
473
472
  };
473
+ schemaVersion: string;
474
474
  id: string;
475
475
  description: string;
476
476
  version: string;
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
- schemaVersion: string;
489
488
  includes: {
490
489
  procedural: boolean;
491
490
  taxonomy: boolean;
492
491
  identityAnchors: boolean;
493
492
  peerProfiles: boolean;
494
493
  };
494
+ schemaVersion: string;
495
495
  id: string;
496
496
  description: string;
497
497
  version: string;
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
- schemaVersion: string;
522
521
  includes: {
523
522
  procedural: boolean;
524
523
  taxonomy: boolean;
525
524
  identityAnchors: boolean;
526
525
  peerProfiles: boolean;
527
526
  };
527
+ schemaVersion: string;
528
528
  id: string;
529
529
  description: string;
530
530
  version: string;
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
- schemaVersion: string;
555
554
  includes: {
556
555
  procedural: boolean;
557
556
  taxonomy: boolean;
558
557
  identityAnchors: boolean;
559
558
  peerProfiles: boolean;
560
559
  };
560
+ schemaVersion: string;
561
561
  id: string;
562
562
  description: string;
563
563
  version: string;
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
- schemaVersion: string;
687
686
  includes: {
688
687
  procedural: boolean;
689
688
  taxonomy: boolean;
690
689
  identityAnchors: boolean;
691
690
  peerProfiles: boolean;
692
691
  };
692
+ schemaVersion: string;
693
693
  id: string;
694
694
  description: string;
695
695
  version: string;
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
- schemaVersion: string;
708
707
  includes: {
709
708
  procedural: boolean;
710
709
  taxonomy: boolean;
711
710
  identityAnchors: boolean;
712
711
  peerProfiles: boolean;
713
712
  };
713
+ schemaVersion: string;
714
714
  id: string;
715
715
  description: string;
716
716
  version: string;
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
- schemaVersion: string;
741
740
  includes: {
742
741
  procedural: boolean;
743
742
  taxonomy: boolean;
744
743
  identityAnchors: boolean;
745
744
  peerProfiles: boolean;
746
745
  };
746
+ schemaVersion: string;
747
747
  id: string;
748
748
  description: string;
749
749
  version: string;
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
- schemaVersion: string;
774
773
  includes: {
775
774
  procedural: boolean;
776
775
  taxonomy: boolean;
777
776
  identityAnchors: boolean;
778
777
  peerProfiles: boolean;
779
778
  };
779
+ schemaVersion: string;
780
780
  id: string;
781
781
  description: string;
782
782
  version: string;
@@ -815,13 +815,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
815
815
  pluginVersion: string;
816
816
  includesTranscripts: boolean;
817
817
  capsule: {
818
- schemaVersion: string;
819
818
  includes: {
820
819
  procedural: boolean;
821
820
  taxonomy: boolean;
822
821
  identityAnchors: boolean;
823
822
  peerProfiles: boolean;
824
823
  };
824
+ schemaVersion: string;
825
825
  id: string;
826
826
  description: string;
827
827
  version: string;
@@ -854,13 +854,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
854
854
  pluginVersion: string;
855
855
  includesTranscripts: boolean;
856
856
  capsule: {
857
- schemaVersion: string;
858
857
  includes: {
859
858
  procedural: boolean;
860
859
  taxonomy: boolean;
861
860
  identityAnchors: boolean;
862
861
  peerProfiles: boolean;
863
862
  };
863
+ schemaVersion: string;
864
864
  id: string;
865
865
  description: string;
866
866
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.601",
3
+ "version": "9.3.602",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/graph.test.ts CHANGED
@@ -1,15 +1,10 @@
1
- import test from "node:test";
2
1
  import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import test from "node:test";
6
6
 
7
- import {
8
- GraphIndex,
9
- graphFilePath,
10
- readEdges,
11
- type GraphConfig,
12
- } from "./graph.js";
7
+ import { type GraphConfig, type GraphEdge, GraphIndex, graphFilePath, readEdges } from "./graph.js";
13
8
 
14
9
  function makeGraphConfig(): GraphConfig {
15
10
  return {
@@ -53,17 +48,87 @@ test("graph reads skip malformed JSON edge objects before traversal", async () =
53
48
  }),
54
49
  "",
55
50
  ].join("\n"),
56
- "utf-8",
51
+ "utf-8"
57
52
  );
58
53
 
59
54
  const edges = await readEdges(memoryDir, "entity");
60
- assert.deepEqual(edges.map((edge) => edge.to), ["c"]);
55
+ assert.deepEqual(
56
+ edges.map((edge) => edge.to),
57
+ ["c"]
58
+ );
61
59
 
62
60
  const graph = new GraphIndex(memoryDir, makeGraphConfig());
63
61
  const activated = await graph.spreadingActivation(["a"]);
64
- assert.deepEqual(activated.map((candidate) => candidate.path), ["c"]);
62
+ assert.deepEqual(
63
+ activated.map((candidate) => candidate.path),
64
+ ["c"]
65
+ );
65
66
  assert.equal(Number.isFinite(activated[0]?.score), true);
66
67
  } finally {
67
68
  await rm(memoryDir, { recursive: true, force: true });
68
69
  }
69
70
  });
71
+
72
+ test("spreadingActivation propagates accumulated activation from multiple seeds", async () => {
73
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-graph-multi-seed-"));
74
+ try {
75
+ await writeGraphEdges(memoryDir, [
76
+ makeEdge("seed-a", "shared"),
77
+ makeEdge("seed-b", "shared"),
78
+ makeEdge("shared", "downstream"),
79
+ ]);
80
+
81
+ const graph = new GraphIndex(memoryDir, makeGraphConfig());
82
+ const activated = await graph.spreadingActivation(["seed-a", "seed-b"]);
83
+ const scores = new Map(activated.map((candidate) => [candidate.path, candidate.score]));
84
+
85
+ assert.equal(scores.get("shared"), 1);
86
+ assert.equal(scores.get("downstream"), 0.5);
87
+ } finally {
88
+ await rm(memoryDir, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ test("spreadingActivation propagates same-depth alternate path activation", async () => {
93
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-graph-alt-path-"));
94
+ try {
95
+ await writeGraphEdges(memoryDir, [
96
+ makeEdge("seed", "left"),
97
+ makeEdge("seed", "right"),
98
+ makeEdge("left", "shared"),
99
+ makeEdge("right", "shared"),
100
+ makeEdge("shared", "downstream"),
101
+ ]);
102
+
103
+ const graph = new GraphIndex(memoryDir, {
104
+ ...makeGraphConfig(),
105
+ maxGraphTraversalSteps: 3,
106
+ });
107
+ const activated = await graph.spreadingActivation(["seed"]);
108
+ const scores = new Map(activated.map((candidate) => [candidate.path, candidate.score]));
109
+
110
+ assert.equal(scores.get("left"), 0.5);
111
+ assert.equal(scores.get("right"), 0.5);
112
+ assert.equal(scores.get("shared"), 0.5);
113
+ assert.equal(scores.get("downstream"), 0.25);
114
+ } finally {
115
+ await rm(memoryDir, { recursive: true, force: true });
116
+ }
117
+ });
118
+
119
+ function makeEdge(from: string, to: string): GraphEdge {
120
+ return {
121
+ from,
122
+ to,
123
+ type: "entity",
124
+ weight: 1,
125
+ label: "test",
126
+ ts: "2026-01-01T00:00:00.000Z",
127
+ };
128
+ }
129
+
130
+ async function writeGraphEdges(memoryDir: string, edges: GraphEdge[]): Promise<void> {
131
+ const filePath = graphFilePath(memoryDir, "entity");
132
+ await mkdir(path.dirname(filePath), { recursive: true });
133
+ await writeFile(filePath, `${edges.map((edge) => JSON.stringify(edge)).join("\n")}\n`, "utf-8");
134
+ }
package/src/graph.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  * All writes are fail-open: errors are caught/logged, never thrown.
11
11
  */
12
12
 
13
- import { mkdir, appendFile, readFile } from "node:fs/promises";
14
- import * as path from "path";
13
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
14
+ import * as path from "node:path";
15
15
 
16
16
  import { readEdgeConfidence } from "./graph-edge-reinforcement.js";
17
17
  import { emitGraphEvent } from "./graph-events.js";
@@ -66,14 +66,7 @@ export const DEFAULT_GRAPH_TRAVERSAL_CONFIDENCE_FLOOR = 0.2;
66
66
  export const DEFAULT_GRAPH_TRAVERSAL_PAGERANK_ITERATIONS = 8;
67
67
 
68
68
  // Causal signal phrases — order matters (most specific first)
69
- export const CAUSAL_PHRASES = [
70
- "as a result",
71
- "led to",
72
- "because of",
73
- "therefore",
74
- "caused",
75
- "because",
76
- ];
69
+ export const CAUSAL_PHRASES = ["as a result", "led to", "because of", "therefore", "caused", "because"];
77
70
 
78
71
  export function graphsDir(memoryDir: string): string {
79
72
  return path.join(memoryDir, "state", "graphs");
@@ -114,8 +107,8 @@ export function withGraphWriteLock<T>(filePath: string, fn: () => Promise<T>): P
114
107
  filePath,
115
108
  next.then(
116
109
  () => undefined,
117
- () => undefined,
118
- ),
110
+ () => undefined
111
+ )
119
112
  );
120
113
  return next;
121
114
  }
@@ -123,7 +116,7 @@ export function withGraphWriteLock<T>(filePath: string, fn: () => Promise<T>): P
123
116
  export async function appendEdge(memoryDir: string, edge: GraphEdge): Promise<void> {
124
117
  await ensureGraphsDir(memoryDir);
125
118
  const filePath = graphFilePath(memoryDir, edge.type);
126
- const line = JSON.stringify(edge) + "\n";
119
+ const line = `${JSON.stringify(edge)}\n`;
127
120
  await withGraphWriteLock(filePath, async () => {
128
121
  await appendFile(filePath, line, "utf8");
129
122
  });
@@ -205,7 +198,7 @@ export async function readEdgesStrict(memoryDir: string, type: GraphType): Promi
205
198
  */
206
199
  export async function readAllEdges(
207
200
  memoryDir: string,
208
- config: Pick<GraphConfig, "entityGraphEnabled" | "timeGraphEnabled" | "causalGraphEnabled">,
201
+ config: Pick<GraphConfig, "entityGraphEnabled" | "timeGraphEnabled" | "causalGraphEnabled">
209
202
  ): Promise<GraphEdge[]> {
210
203
  const parts: GraphEdge[][] = await Promise.all([
211
204
  config.entityGraphEnabled ? readEdges(memoryDir, "entity") : Promise.resolve([]),
@@ -243,9 +236,12 @@ function isValidGraphEdge(raw: unknown, expectedType: GraphType): raw is GraphEd
243
236
  const edge = raw as Record<string, unknown>;
244
237
  return (
245
238
  edge.type === expectedType &&
246
- typeof edge.from === "string" && edge.from.length > 0 &&
247
- typeof edge.to === "string" && edge.to.length > 0 &&
248
- typeof edge.weight === "number" && Number.isFinite(edge.weight) &&
239
+ typeof edge.from === "string" &&
240
+ edge.from.length > 0 &&
241
+ typeof edge.to === "string" &&
242
+ edge.to.length > 0 &&
243
+ typeof edge.weight === "number" &&
244
+ Number.isFinite(edge.weight) &&
249
245
  typeof edge.label === "string" &&
250
246
  typeof edge.ts === "string"
251
247
  );
@@ -258,7 +254,7 @@ export async function analyzeGraphHealth(
258
254
  timeGraphEnabled?: boolean;
259
255
  causalGraphEnabled?: boolean;
260
256
  includeRepairGuidance?: boolean;
261
- },
257
+ }
262
258
  ): Promise<GraphHealthReport> {
263
259
  const enabledTypes: GraphType[] = [];
264
260
  if (options?.entityGraphEnabled !== false) enabledTypes.push("entity");
@@ -324,7 +320,7 @@ export async function analyzeGraphHealth(
324
320
  validEdges: 0,
325
321
  corruptLines: 0,
326
322
  uniqueNodes: globalNodes.size,
327
- },
323
+ }
328
324
  );
329
325
  totals.uniqueNodes = globalNodes.size;
330
326
 
@@ -338,10 +334,14 @@ export async function analyzeGraphHealth(
338
334
  if (options?.includeRepairGuidance === true) {
339
335
  const guidance: string[] = [];
340
336
  if (totals.corruptLines > 0) {
341
- guidance.push("Corrupt graph lines detected: back up memory/state/graphs, then rebuild graphs from clean memory replay/extraction runs.");
337
+ guidance.push(
338
+ "Corrupt graph lines detected: back up memory/state/graphs, then rebuild graphs from clean memory replay/extraction runs."
339
+ );
342
340
  }
343
341
  if (totals.validEdges === 0) {
344
- guidance.push("No valid edges detected yet: run normal extraction traffic (or replay ingestion) to seed graph files.");
342
+ guidance.push(
343
+ "No valid edges detected yet: run normal extraction traffic (or replay ingestion) to seed graph files."
344
+ );
345
345
  }
346
346
  if (guidance.length > 0) report.repairGuidance = guidance;
347
347
  }
@@ -392,10 +392,7 @@ export class GraphIndex {
392
392
  }
393
393
 
394
394
  private async loadEdgesCached(): Promise<GraphEdge[]> {
395
- if (
396
- this.edgeCache &&
397
- Date.now() - this.edgeCache.loadedAt < GraphIndex.EDGE_CACHE_TTL_MS
398
- ) {
395
+ if (this.edgeCache && Date.now() - this.edgeCache.loadedAt < GraphIndex.EDGE_CACHE_TTL_MS) {
399
396
  return this.edgeCache.allEdges;
400
397
  }
401
398
  const allEdges = await readAllEdges(this.memoryDir, {
@@ -520,21 +517,23 @@ export class GraphIndex {
520
517
  * Default `false` (floor from config is applied).
521
518
  */
522
519
  includeLowConfidence?: boolean;
523
- },
524
- ): Promise<Array<{
525
- path: string;
526
- score: number;
527
- seed: string;
528
- hopDepth: number;
529
- decayedWeight: number;
530
- graphType: "entity" | "time" | "causal";
531
- /**
532
- * Confidence of the edge that produced this candidate's recorded
533
- * provenance (the strongest edge along the chosen entry path).
534
- * In `[0, 1]`. Legacy edges without `confidence` surface as 1.0.
535
- */
536
- edgeConfidence: number;
537
- }>> {
520
+ }
521
+ ): Promise<
522
+ Array<{
523
+ path: string;
524
+ score: number;
525
+ seed: string;
526
+ hopDepth: number;
527
+ decayedWeight: number;
528
+ graphType: "entity" | "time" | "causal";
529
+ /**
530
+ * Confidence of the edge that produced this candidate's recorded
531
+ * provenance (the strongest edge along the chosen entry path).
532
+ * In `[0, 1]`. Legacy edges without `confidence` surface as 1.0.
533
+ */
534
+ edgeConfidence: number;
535
+ }>
536
+ > {
538
537
  if (!this.cfg.multiGraphMemoryEnabled) return [];
539
538
  const steps = maxSteps ?? this.cfg.maxGraphTraversalSteps;
540
539
  const decay = this.cfg.graphActivationDecay;
@@ -543,12 +542,9 @@ export class GraphIndex {
543
542
  // Otherwise clamp the configured floor into [0, 1] so misconfiguration
544
543
  // cannot (a) admit edges with negative confidence or (b) reject every
545
544
  // edge.
546
- const floor = opts?.includeLowConfidence === true
547
- ? 0
548
- : clampConfidenceFloor(this.cfg.graphTraversalConfidenceFloor);
549
- const iterations = clampPageRankIterations(
550
- this.cfg.graphTraversalPageRankIterations,
551
- );
545
+ const floor =
546
+ opts?.includeLowConfidence === true ? 0 : clampConfidenceFloor(this.cfg.graphTraversalConfidenceFloor);
547
+ const iterations = clampPageRankIterations(this.cfg.graphTraversalPageRankIterations);
552
548
 
553
549
  try {
554
550
  const allEdges = await this.loadEdgesCached();
@@ -581,50 +577,67 @@ export class GraphIndex {
581
577
  edgeConfidence: number;
582
578
  }
583
579
  >();
584
- const visited = new Set<string>(seeds);
585
-
586
- // BFS queue: [nodePath, hop, seedPath]
587
- const queue: Array<[string, number, string]> = seeds.map((s) => [s, 0, s]);
588
-
589
- while (queue.length > 0) {
590
- const [node, hop, sourceSeed] = queue.shift()!;
591
- if (hop >= steps) continue;
592
-
593
- const edges = adj.get(node) ?? [];
594
- for (const edge of edges) {
595
- const neighbor = edge.to === node ? edge.from : edge.to;
596
- const conf = readEdgeConfidence(edge);
597
- // Defense in depth: the adjacency build already drops sub-floor
598
- // edges, but if a synthesized reverse edge ever bypassed that
599
- // path, this guard keeps spreading activation honest.
600
- if (conf < floor) continue;
601
- const score = edge.weight * conf * Math.pow(decay, hop + 1);
602
-
603
- if (!seedSet.has(neighbor)) {
604
- const existing = scores.get(neighbor) ?? 0;
605
- scores.set(neighbor, existing + score);
606
-
607
- const prev = provenance.get(neighbor);
608
- if (
609
- !prev ||
610
- hop + 1 < prev.hopDepth ||
611
- (hop + 1 === prev.hopDepth && score > prev.decayedWeight)
612
- ) {
613
- provenance.set(neighbor, {
614
- seed: sourceSeed,
615
- hopDepth: hop + 1,
616
- decayedWeight: score,
617
- graphType: edge.type,
618
- edgeConfidence: conf,
619
- });
580
+ let frontier = new Map<string, { node: string; seed: string; activation: number }>();
581
+ const reachedBySeed = new Map<string, Set<string>>();
582
+ for (const seed of seeds) {
583
+ frontier.set(`${seed}\0${seed}`, { node: seed, seed, activation: 1 });
584
+ reachedBySeed.set(seed, new Set([seed]));
585
+ }
586
+
587
+ for (let hop = 0; hop < steps && frontier.size > 0; hop++) {
588
+ const nextFrontier = new Map<string, { node: string; seed: string; activation: number }>();
589
+
590
+ for (const { node, seed: sourceSeed, activation } of frontier.values()) {
591
+ const edges = adj.get(node) ?? [];
592
+ for (const edge of edges) {
593
+ const neighbor = edge.to === node ? edge.from : edge.to;
594
+ const conf = readEdgeConfidence(edge);
595
+ // Defense in depth: the adjacency build already drops sub-floor
596
+ // edges, but if a synthesized reverse edge ever bypassed that
597
+ // path, this guard keeps spreading activation honest.
598
+ if (conf < floor) continue;
599
+ const score = activation * edge.weight * conf * decay;
600
+ const reachedForSeed = reachedBySeed.get(sourceSeed);
601
+ if (reachedForSeed?.has(neighbor)) {
602
+ continue;
620
603
  }
621
- }
622
604
 
623
- if (!visited.has(neighbor)) {
624
- visited.add(neighbor);
625
- queue.push([neighbor, hop + 1, sourceSeed]);
605
+ if (!seedSet.has(neighbor)) {
606
+ const existing = scores.get(neighbor) ?? 0;
607
+ scores.set(neighbor, existing + score);
608
+
609
+ const prev = provenance.get(neighbor);
610
+ if (!prev || hop + 1 < prev.hopDepth || (hop + 1 === prev.hopDepth && score > prev.decayedWeight)) {
611
+ provenance.set(neighbor, {
612
+ seed: sourceSeed,
613
+ hopDepth: hop + 1,
614
+ decayedWeight: score,
615
+ graphType: edge.type,
616
+ edgeConfidence: conf,
617
+ });
618
+ }
619
+
620
+ if (hop + 1 < steps) {
621
+ const frontierKey = `${sourceSeed}\0${neighbor}`;
622
+ const existingFrontier = nextFrontier.get(frontierKey);
623
+ if (existingFrontier) {
624
+ existingFrontier.activation += score;
625
+ } else {
626
+ nextFrontier.set(frontierKey, {
627
+ node: neighbor,
628
+ seed: sourceSeed,
629
+ activation: score,
630
+ });
631
+ }
632
+ }
633
+ }
626
634
  }
627
635
  }
636
+
637
+ for (const { node, seed } of nextFrontier.values()) {
638
+ reachedBySeed.get(seed)?.add(node);
639
+ }
640
+ frontier = nextFrontier;
628
641
  }
629
642
 
630
643
  // Issue #681 PR 3/3 — optional PageRank-style refinement.
@@ -712,7 +725,7 @@ export function clampPageRankIterations(raw: unknown): number {
712
725
  export function applyPageRankRefinement(
713
726
  scores: Map<string, number>,
714
727
  adj: Map<string, GraphEdge[]>,
715
- opts: { iterations: number; floor: number; damping: number },
728
+ opts: { iterations: number; floor: number; damping: number }
716
729
  ): void {
717
730
  const { iterations, floor, damping } = opts;
718
731
  if (iterations <= 0 || scores.size === 0) return;
@@ -793,7 +806,7 @@ export function applyPageRankRefinement(
793
806
  */
794
807
  export function applyLateralInhibition(
795
808
  scores: Map<string, number>,
796
- opts: { beta: number; topM: number },
809
+ opts: { beta: number; topM: number }
797
810
  ): Map<string, number> {
798
811
  const { beta, topM } = opts;
799
812
  if (beta === 0 || topM === 0) return new Map(scores);