@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Result, succeed } from '@fgv/ts-utils';
7
- import { IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag } from '../types';
7
+ import { IEdgeTarget, IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag, edgeTargetKey } from '../types';
8
8
 
9
9
  /**
10
10
  * The mutation a {@link IMemoryIndex.patch | patch} applies: a record was
@@ -72,10 +72,21 @@ export interface IMemoryIndex {
72
72
  byRecency(): ReadonlyArray<IMemoryRecord<unknown>>;
73
73
 
74
74
  /**
75
- * The ids of records whose `links` point AT `target` (inbound edges).
76
- * The seed map for B2 link-traversal.
75
+ * All records ordered by store-computed {@link IMemoryEnvelope.rank} descending,
76
+ * with recency (most-recently-updated, then `seq`) as a tiebreak. Records with
77
+ * an absent `rank` sort LAST (after every ranked record), then by recency among
78
+ * themselves. Serves a bounded top-M ({@link IMemoryEnvelope.rank}-ordered) page
79
+ * from the in-memory index with no full-vault (filesystem) scan.
77
80
  */
78
- backlinks(target: MemoryId): ReadonlyArray<MemoryId>;
81
+ byRank(): ReadonlyArray<IMemoryRecord<unknown>>;
82
+
83
+ /**
84
+ * The scope-qualified sources of records whose `links` point AT `target`
85
+ * (inbound edges), keyed on the target's `(scope, id)` address. The seed map
86
+ * for B2 link-traversal; results are {@link IEdgeTarget}s so a caller can feed
87
+ * them straight back in as further traversal seeds.
88
+ */
89
+ backlinks(target: IEdgeTarget): ReadonlyArray<IEdgeTarget>;
79
90
  }
80
91
 
81
92
  /**
@@ -93,18 +104,21 @@ export class MemoryIndex implements IMemoryIndex {
93
104
  /** tag → set of composite keys. */
94
105
  private readonly _byTag: Map<Tag, Set<string>>;
95
106
  /**
96
- * link target id → (source composite key → source id). Keyed by the source's
97
- * `(scope, id)` composite NOT its bare id so two distinct source records
98
- * that share an id across scopes (e.g. `turn-0` in different conversations)
99
- * are tracked independently and removing one never drops the other's edge.
107
+ * canonical target key (`edgeTargetKey`) → (source composite key → source
108
+ * {@link IEdgeTarget}). The OUTER map is keyed on the scope-qualified target's
109
+ * canonical `(scope, id)` string NOT the target's bare id — so an edge to
110
+ * `turn-3` in one conversation is tracked separately from `turn-3` in another.
111
+ * The INNER map is keyed by the source's `(scope, id)` composite so two distinct
112
+ * source records that share an id across scopes are tracked independently and
113
+ * removing one never drops the other's edge.
100
114
  */
101
- private readonly _backlinks: Map<MemoryId, Map<string, MemoryId>>;
115
+ private readonly _backlinks: Map<string, Map<string, IEdgeTarget>>;
102
116
 
103
117
  private constructor() {
104
118
  this._byKey = new Map<string, IIndexedMemoryRecord>();
105
119
  this._byKind = new Map<Kind, Set<string>>();
106
120
  this._byTag = new Map<Tag, Set<string>>();
107
- this._backlinks = new Map<MemoryId, Map<string, MemoryId>>();
121
+ this._backlinks = new Map<string, Map<string, IEdgeTarget>>();
108
122
  }
109
123
 
110
124
  /** Family-convention factory. */
@@ -119,7 +133,7 @@ export class MemoryIndex implements IMemoryIndex {
119
133
  * is a collision-proof separator across every scope/id pair the codecs produce.
120
134
  */
121
135
  private static _keyOf(scope: MemoryScopeKey, id: MemoryId): string {
122
- return `${scope}\0${id}`;
136
+ return edgeTargetKey({ scope, id });
123
137
  }
124
138
 
125
139
  /** {@inheritDoc IMemoryIndex.rebuild} */
@@ -166,9 +180,14 @@ export class MemoryIndex implements IMemoryIndex {
166
180
  return this._recencyOrdered(this._byKey.keys());
167
181
  }
168
182
 
183
+ /** {@inheritDoc IMemoryIndex.byRank} */
184
+ public byRank(): ReadonlyArray<IMemoryRecord<unknown>> {
185
+ return this._rankOrdered(this._byKey.keys());
186
+ }
187
+
169
188
  /** {@inheritDoc IMemoryIndex.backlinks} */
170
- public backlinks(target: MemoryId): ReadonlyArray<MemoryId> {
171
- const sources: Map<string, MemoryId> | undefined = this._backlinks.get(target);
189
+ public backlinks(target: IEdgeTarget): ReadonlyArray<IEdgeTarget> {
190
+ const sources: Map<string, IEdgeTarget> | undefined = this._backlinks.get(edgeTargetKey(target));
172
191
  return sources === undefined ? [] : Array.from(sources.values());
173
192
  }
174
193
 
@@ -191,6 +210,49 @@ export class MemoryIndex implements IMemoryIndex {
191
210
  });
192
211
  }
193
212
 
213
+ /**
214
+ * Resolve a set of composite keys to their records, ordered by
215
+ * {@link IMemoryEnvelope.rank} descending with recency (`updated`, then `seq`)
216
+ * as the tiebreak. Records with an absent `rank` sort LAST, then by recency
217
+ * among themselves. Computed on call (mirrors {@link MemoryIndex._recencyOrdered}) —
218
+ * no incremental rank-ordered view is maintained, matching the recency view's
219
+ * approach; the sort is over the in-memory index, never a filesystem walk.
220
+ */
221
+ private _rankOrdered(keys: Iterable<string>): ReadonlyArray<IMemoryRecord<unknown>> {
222
+ const records: IMemoryRecord<unknown>[] = [];
223
+ for (const key of keys) {
224
+ const entry: IIndexedMemoryRecord | undefined = this._byKey.get(key);
225
+ if (entry !== undefined) {
226
+ records.push(entry.record);
227
+ }
228
+ }
229
+ return records.sort(MemoryIndex._compareByRank);
230
+ }
231
+
232
+ /**
233
+ * Rank-descending comparator with an absent-`rank`-last rule and a recency
234
+ * (`updated`, then `seq`) tiebreak. Duplicated from the retrieve packlet's
235
+ * `rankCompare` deliberately: the index must not depend on `retrieve` (that
236
+ * package depends on the index), mirroring how `_recencyOrdered` inlines the
237
+ * recency ordering rather than importing `recencyCompare`.
238
+ */
239
+ private static _compareByRank(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {
240
+ const ra: number | undefined = a.envelope.rank;
241
+ const rb: number | undefined = b.envelope.rank;
242
+ // Absent rank sorts last; two absent ranks fall through to the recency tiebreak.
243
+ if (ra === undefined && rb !== undefined) {
244
+ return 1;
245
+ }
246
+ if (rb === undefined && ra !== undefined) {
247
+ return -1;
248
+ }
249
+ if (ra !== undefined && rb !== undefined && ra !== rb) {
250
+ return rb - ra;
251
+ }
252
+ const byUpdated: number = b.envelope.updated - a.envelope.updated;
253
+ return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;
254
+ }
255
+
194
256
  /** Insert an entry and register all its derived associations. */
195
257
  private _add(entry: IIndexedMemoryRecord): void {
196
258
  const key: string = MemoryIndex._keyOf(entry.scope, entry.record.envelope.id);
@@ -201,7 +263,7 @@ export class MemoryIndex implements IMemoryIndex {
201
263
  this._addToSetMap(this._byTag, tag, key);
202
264
  }
203
265
  for (const edge of envelope.links) {
204
- this._addBacklink(edge.target, key, envelope.id);
266
+ this._addBacklink(edge.target, key, { scope: entry.scope, id: envelope.id });
205
267
  }
206
268
  }
207
269
 
@@ -222,25 +284,27 @@ export class MemoryIndex implements IMemoryIndex {
222
284
  }
223
285
  }
224
286
 
225
- /** Register `sourceId` (keyed by its composite `sourceKey`) as linking at `target`. */
226
- private _addBacklink(target: MemoryId, sourceKey: string, sourceId: MemoryId): void {
227
- const existing: Map<string, MemoryId> | undefined = this._backlinks.get(target);
287
+ /** Register `source` (keyed by its composite `sourceKey`) as linking at `target`. */
288
+ private _addBacklink(target: IEdgeTarget, sourceKey: string, source: IEdgeTarget): void {
289
+ const targetKey: string = edgeTargetKey(target);
290
+ const existing: Map<string, IEdgeTarget> | undefined = this._backlinks.get(targetKey);
228
291
  if (existing === undefined) {
229
- this._backlinks.set(target, new Map<string, MemoryId>([[sourceKey, sourceId]]));
292
+ this._backlinks.set(targetKey, new Map<string, IEdgeTarget>([[sourceKey, source]]));
230
293
  } else {
231
- existing.set(sourceKey, sourceId);
294
+ existing.set(sourceKey, source);
232
295
  }
233
296
  }
234
297
 
235
298
  /** Drop the backlink from `sourceKey` to `target`, removing the target map when empty. */
236
- private _removeBacklink(target: MemoryId, sourceKey: string): void {
237
- const existing: Map<string, MemoryId> | undefined = this._backlinks.get(target);
299
+ private _removeBacklink(target: IEdgeTarget, sourceKey: string): void {
300
+ const targetKey: string = edgeTargetKey(target);
301
+ const existing: Map<string, IEdgeTarget> | undefined = this._backlinks.get(targetKey);
238
302
  if (existing === undefined) {
239
303
  return;
240
304
  }
241
305
  existing.delete(sourceKey);
242
306
  if (existing.size === 0) {
243
- this._backlinks.delete(target);
307
+ this._backlinks.delete(targetKey);
244
308
  }
245
309
  }
246
310
 
@@ -4,19 +4,24 @@
4
4
  */
5
5
 
6
6
  import { Hash, Result, fail, mapResults, succeed } from '@fgv/ts-utils';
7
- import { LinkType, MemoryId } from '../types';
7
+ import { IEdgeTarget, LinkType, edgeTargetKey } from '../types';
8
8
 
9
9
  /**
10
- * A directed edge in the link graph the cycle guard reasons over: `source` links
11
- * to `target` under relation `type`.
10
+ * A directed edge in the link graph the cycle guard reasons over: scope-qualified
11
+ * `source` links to scope-qualified `target` under relation `type`.
12
12
  * @public
13
13
  */
14
14
  export interface ICycleGuardEdge {
15
- readonly source: MemoryId;
16
- readonly target: MemoryId;
15
+ readonly source: IEdgeTarget;
16
+ readonly target: IEdgeTarget;
17
17
  readonly type: LinkType;
18
18
  }
19
19
 
20
+ /** Human-readable `scope/id` rendering of a scoped node, for cycle-guard diagnostics. */
21
+ function formatNode(node: IEdgeTarget): string {
22
+ return `${node.scope}/${node.id}`;
23
+ }
24
+
20
25
  /**
21
26
  * Build the design's `buildCycleKey` — a deterministic, canonical (RFC-8785,
22
27
  * via `Crc32Normalizer` from `@fgv/ts-utils`) identity for a directed edge. Used
@@ -69,17 +74,23 @@ export function assertNoCycles(
69
74
  }
70
75
  for (const keyed of proposedKeyed) {
71
76
  const edge: ICycleGuardEdge = keyed.edge;
77
+ const sourceKey: string = edgeTargetKey(edge.source);
78
+ const targetKey: string = edgeTargetKey(edge.target);
72
79
  // A self-loop is the degenerate one-node cycle.
73
- if (edge.source === edge.target) {
80
+ if (sourceKey === targetKey) {
74
81
  return fail(
75
- `ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' is a self-loop`
82
+ `ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(
83
+ edge.target
84
+ )}' is a self-loop`
76
85
  );
77
86
  }
78
87
  // Reachability: does `target` already reach `source`? If so, adding
79
88
  // `source -> target` closes a directed cycle.
80
- if (reaches(adjacency, edge.target, edge.source)) {
89
+ if (reaches(adjacency, targetKey, sourceKey)) {
81
90
  return fail(
82
- `ingest cycle guard: edge '${edge.source}' -${edge.type}-> '${edge.target}' would create a cycle`
91
+ `ingest cycle guard: edge '${formatNode(edge.source)}' -${edge.type}-> '${formatNode(
92
+ edge.target
93
+ )}' would create a cycle`
83
94
  );
84
95
  }
85
96
  addEdge(adjacency, seenKeys, keyed);
@@ -106,8 +117,8 @@ function addEdge(adjacency: Map<string, Set<string>>, seenKeys: Set<string>, key
106
117
  return;
107
118
  }
108
119
  seenKeys.add(keyed.key);
109
- const source: string = keyed.edge.source;
110
- const target: string = keyed.edge.target;
120
+ const source: string = edgeTargetKey(keyed.edge.source);
121
+ const target: string = edgeTargetKey(keyed.edge.target);
111
122
  const targets: Set<string> | undefined = adjacency.get(source);
112
123
  if (targets === undefined) {
113
124
  adjacency.set(source, new Set<string>([target]));
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Result } from '@fgv/ts-utils';
7
- import { MemoryId } from '../types';
7
+ import { IEdgeTarget } from '../types';
8
8
  import {
9
9
  ICandidateEdge,
10
10
  ICandidateRecord,
@@ -90,8 +90,8 @@ export interface IRelationContext {
90
90
  export interface IRelationCandidate {
91
91
  /** The candidate about to be written. */
92
92
  readonly candidate: ICandidateRecord;
93
- /** Its resolved reference id (codec `idStem` — the stable entity reference). */
94
- readonly id: MemoryId;
93
+ /** Its resolved scope-qualified reference (codec `(scope, idStem)` — the stable entity address). */
94
+ readonly id: IEdgeTarget;
95
95
  }
96
96
 
97
97
  /**
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
 
6
- import { IEdge, IMemoryEnvelope, IMemoryRecord, Kind, MemoryId, Tag } from '../types';
6
+ import { IEdge, IEdgeTarget, IMemoryEnvelope, IMemoryRecord, Kind, MemoryId, Tag } from '../types';
7
7
 
8
8
  /**
9
9
  * A single unit of raw source material handed to the ingest pipeline. The host
@@ -28,12 +28,15 @@ export interface IIngestItem {
28
28
  */
29
29
  readonly content: unknown;
30
30
  /**
31
- * Optional back-link to the memory record this item was derived from (e.g. the
32
- * MTM turn an extracted fact came from). When present, fgv stamps it as
33
- * {@link IProvenance.derivedFrom | provenance.derivedFrom} on every record
34
- * ingested from this item (stage 6) — the cross-kind provenance spine.
31
+ * Optional scope-qualified back-link to the memory record this item was derived
32
+ * from (e.g. the MTM turn an extracted fact came from). When present, fgv stamps
33
+ * it as {@link IProvenance.derivedFrom | provenance.derivedFrom} on every record
34
+ * ingested from this item (stage 6) — the cross-kind provenance spine. A
35
+ * scope-qualified {@link IEdgeTarget} (not a bare {@link MemoryId}) because
36
+ * per-scope codecs legally reuse a stem across scopes, so a bare id would be
37
+ * ambiguous.
35
38
  */
36
- readonly sourceId?: MemoryId;
39
+ readonly sourceId?: IEdgeTarget;
37
40
  /** Optional opaque metadata carried alongside the item; never interpreted by fgv. */
38
41
  readonly metadata?: Record<string, unknown>;
39
42
  }
@@ -88,8 +91,14 @@ export interface ICandidateRecord {
88
91
  * @public
89
92
  */
90
93
  export interface IEntityResolutionCandidate {
91
- /** The existing record's id. */
92
- readonly id: MemoryId;
94
+ /**
95
+ * The existing record's scope-qualified `(scope, id)` address. Scope-qualified
96
+ * (not a bare {@link MemoryId}) because per-scope codecs legally mint the same
97
+ * stem under different scopes — the {@link ResolutionVerdict} target the resolver
98
+ * returns must round-trip this exact address so the write binds the intended
99
+ * record.
100
+ */
101
+ readonly target: IEdgeTarget;
93
102
  /** The existing record. */
94
103
  readonly record: IMemoryRecord<unknown>;
95
104
  /** The backend similarity score (higher = more similar). */
@@ -99,14 +108,16 @@ export interface IEntityResolutionCandidate {
99
108
  /**
100
109
  * The four dedup verdicts a {@link IEntityResolver} (or fgv's exact-match layer)
101
110
  * returns for a candidate. See the design note §3 for the verdict → write
102
- * disposition mapping.
111
+ * disposition mapping. Each target-bearing arm carries a scope-qualified
112
+ * {@link IEdgeTarget} (not a bare {@link MemoryId}) so the verdict resolves to a
113
+ * single record even when a filename stem is reused across scopes.
103
114
  * @public
104
115
  */
105
116
  export type ResolutionVerdict =
106
117
  | { readonly verdict: 'new' }
107
- | { readonly verdict: 'duplicate-of'; readonly target: MemoryId }
108
- | { readonly verdict: 'supersede'; readonly target: MemoryId }
109
- | { readonly verdict: 'merge-into'; readonly target: MemoryId };
118
+ | { readonly verdict: 'duplicate-of'; readonly target: IEdgeTarget }
119
+ | { readonly verdict: 'supersede'; readonly target: IEdgeTarget }
120
+ | { readonly verdict: 'merge-into'; readonly target: IEdgeTarget };
110
121
 
111
122
  /**
112
123
  * How a candidate was ultimately written (or not) after resolution.
@@ -129,8 +140,8 @@ export type IngestDisposition = 'written' | 'deduped' | 'merged';
129
140
  * @public
130
141
  */
131
142
  export interface ICandidateEdge {
132
- /** The reference id (codec `idStem`) of the candidate the edge originates from. */
133
- readonly source: MemoryId;
143
+ /** The scope-qualified reference (codec `(scope, idStem)`) of the candidate the edge originates from. */
144
+ readonly source: IEdgeTarget;
134
145
  /** The attributed edge (type / target / optional confidence / provenance). */
135
146
  readonly edge: IEdge;
136
147
  }