@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
@@ -1,5 +1,5 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { MemoryId } from '../types';
2
+ import { IEdgeTarget } from '../types';
3
3
  import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, MemoryEmbedder } from './vectorIndex';
4
4
  /**
5
5
  * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one
@@ -25,6 +25,11 @@ import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, MemoryEmbedder } fr
25
25
  * @public
26
26
  */
27
27
  export declare class InMemoryCosineIndex implements IVectorIndex {
28
+ /**
29
+ * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the
30
+ * record's scope-qualified address, so two records that share a filename stem
31
+ * across scopes occupy distinct entries and never overwrite each other.
32
+ */
28
33
  private readonly _vectors;
29
34
  /** The dimension of every stored vector; `undefined` until the first `add`. */
30
35
  private _dimension;
@@ -34,9 +39,9 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
34
39
  /** Family-convention factory. */
35
40
  static create(): Result<InMemoryCosineIndex>;
36
41
  /** {@inheritDoc IVectorIndex.add} */
37
- add(id: MemoryId, vector: Float32Array): Promise<Result<string>>;
42
+ add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
38
43
  /** {@inheritDoc IVectorIndex.remove} */
39
- remove(id: MemoryId): Promise<Result<MemoryId>>;
44
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
40
45
  /** {@inheritDoc IVectorIndex.query} */
41
46
  query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
42
47
  /**
@@ -49,8 +54,7 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
49
54
  * rather than left in a partially-rebuilt state — a caller that retries a query
50
55
  * after a failed rebuild sees a clean empty index, never a half-populated one.
51
56
  *
52
- * @param source - The record source to re-embed (an {@link IMemoryStore}
53
- * satisfies this structurally).
57
+ * @param source - The scope-qualified record source to re-embed.
54
58
  * @param embed - The embedder applied to each record.
55
59
  */
56
60
  rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder): Promise<Result<number>>;
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAiB,QAAQ,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEnG;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAoBvE,wCAAwC;IACjC,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAKtD,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAsBjG;;;;;;;;;;;;;OAaG;IACU,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAwBjG,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,mBAAmB,EAEnB,YAAY,EACZ,eAAe,EACf,cAAc,EACf,MAAM,eAAe,CAAC;AAQvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAsB9E,wCAAwC;IACjC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyBjG;;;;;;;;;;;;OAYG;IACU,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA0BjG,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.InMemoryCosineIndex = void 0;
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
+ const types_1 = require("../types");
9
10
  /**
10
11
  * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one
11
12
  * `Float32Array` per record and answers a query by computing cosine similarity
@@ -43,26 +44,28 @@ class InMemoryCosineIndex {
43
44
  return (0, ts_utils_1.succeed)(new InMemoryCosineIndex());
44
45
  }
45
46
  /** {@inheritDoc IVectorIndex.add} */
46
- add(id, vector) {
47
+ add(target, vector) {
48
+ const key = (0, types_1.edgeTargetKey)(target);
47
49
  if (vector.length === 0) {
48
- return Promise.resolve((0, ts_utils_1.fail)(`vector index: cannot add '${id}': empty vector`));
50
+ return Promise.resolve((0, ts_utils_1.fail)(`vector index: cannot add '${key}': empty vector`));
49
51
  }
50
52
  if (this._dimension === undefined) {
51
53
  this._dimension = vector.length;
52
54
  }
53
55
  else if (vector.length !== this._dimension) {
54
- return Promise.resolve((0, ts_utils_1.fail)(`vector index: cannot add '${id}': dimension ${vector.length} does not match index dimension ${this._dimension}`));
56
+ return Promise.resolve((0, ts_utils_1.fail)(`vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`));
55
57
  }
56
58
  // Defensive copy: the caller may reuse or mutate the buffer after `add`, and
57
59
  // the index must keep serving the embedding it was given.
58
- this._vectors.set(id, Float32Array.from(vector));
59
- // The in-memory index keys entries by id, so the id IS the entry reference.
60
- return Promise.resolve((0, ts_utils_1.succeed)(id));
60
+ this._vectors.set(key, { target, vector: Float32Array.from(vector) });
61
+ // The in-memory index keys entries by the canonical scoped-target string, so
62
+ // that key IS the entry reference.
63
+ return Promise.resolve((0, ts_utils_1.succeed)(key));
61
64
  }
62
65
  /** {@inheritDoc IVectorIndex.remove} */
63
- remove(id) {
64
- this._vectors.delete(id);
65
- return Promise.resolve((0, ts_utils_1.succeed)(id));
66
+ remove(target) {
67
+ this._vectors.delete((0, types_1.edgeTargetKey)(target));
68
+ return Promise.resolve((0, ts_utils_1.succeed)(target));
66
69
  }
67
70
  /** {@inheritDoc IVectorIndex.query} */
68
71
  query(vector, topK) {
@@ -74,8 +77,11 @@ class InMemoryCosineIndex {
74
77
  }
75
78
  const queryMagnitude = InMemoryCosineIndex._magnitude(vector);
76
79
  const hits = [];
77
- for (const [id, stored] of this._vectors) {
78
- hits.push({ id, score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored) });
80
+ for (const stored of this._vectors.values()) {
81
+ hits.push({
82
+ target: stored.target,
83
+ score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)
84
+ });
79
85
  }
80
86
  // Descending by score; a `seq`-free tiebreak is unnecessary here because the
81
87
  // caller (SemanticRetriever) re-resolves hits against the record index.
@@ -92,8 +98,7 @@ class InMemoryCosineIndex {
92
98
  * rather than left in a partially-rebuilt state — a caller that retries a query
93
99
  * after a failed rebuild sees a clean empty index, never a half-populated one.
94
100
  *
95
- * @param source - The record source to re-embed (an {@link IMemoryStore}
96
- * satisfies this structurally).
101
+ * @param source - The scope-qualified record source to re-embed.
97
102
  * @param embed - The embedder applied to each record.
98
103
  */
99
104
  async rebuild(source, embed) {
@@ -105,13 +110,13 @@ class InMemoryCosineIndex {
105
110
  if (listed.isFailure()) {
106
111
  return (0, ts_utils_1.fail)(`vector index rebuild: failed to list records: ${listed.message}`);
107
112
  }
108
- for (const record of listed.value) {
109
- const embedded = await embed(record);
113
+ for (const scoped of listed.value) {
114
+ const embedded = await embed(scoped.record);
110
115
  if (embedded.isFailure()) {
111
116
  this._reset();
112
- return (0, ts_utils_1.fail)(`vector index rebuild: embedding '${record.envelope.id}' failed: ${embedded.message}`);
117
+ return (0, ts_utils_1.fail)(`vector index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`);
113
118
  }
114
- const added = await this.add(record.envelope.id, embedded.value);
119
+ const added = await this.add(scoped.target, embedded.value);
115
120
  if (added.isFailure()) {
116
121
  this._reset();
117
122
  return (0, ts_utils_1.fail)(`vector index rebuild: ${added.message}`);
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\nimport { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, MemoryEmbedder } from './vectorIndex';\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n private readonly _vectors: Map<MemoryId, Float32Array>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<MemoryId, Float32Array>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(id: MemoryId, vector: Float32Array): Promise<Result<string>> {\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${id}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${id}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(id, Float32Array.from(vector));\n // The in-memory index keys entries by id, so the id IS the entry reference.\n return Promise.resolve(succeed(id as string));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(id: MemoryId): Promise<Result<MemoryId>> {\n this._vectors.delete(id);\n return Promise.resolve(succeed(id));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const [id, stored] of this._vectors) {\n hits.push({ id, score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored) });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns the number of vectors\n * indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty\n * rather than left in a partially-rebuilt state — a caller that retries a query\n * after a failed rebuild sees a clean empty index, never a half-populated one.\n *\n * @param source - The record source to re-embed (an {@link IMemoryStore}\n * satisfies this structurally).\n * @param embed - The embedder applied to each record.\n */\n public async rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder): Promise<Result<number>> {\n // Reset up front so the \"any failure leaves the index empty\" contract holds\n // even when the listing itself fails (no stale vectors survive a failed\n // rebuild).\n this._reset();\n const listed: Result<ReadonlyArray<IMemoryRecord<unknown>>> = await source.list();\n if (listed.isFailure()) {\n return fail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n for (const record of listed.value) {\n const embedded: Result<Float32Array> = await embed(record);\n if (embedded.isFailure()) {\n this._reset();\n return fail(`vector index rebuild: embedding '${record.envelope.id}' failed: ${embedded.message}`);\n }\n const added: Result<string> = await this.add(record.envelope.id, embedded.value);\n if (added.isFailure()) {\n this._reset();\n return fail(`vector index rebuild: ${added.message}`);\n }\n }\n return succeed(this._vectors.size);\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
1
+ {"version":3,"file":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAsD;AACtD,oCAAsD;AAetD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAa,mBAAmB;IAU9B;QACE,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;QACjD,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,4CAA4C;IAC5C,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM;QAClB,OAAO,IAAA,kBAAO,EAAC,IAAI,mBAAmB,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,qCAAqC;IAC9B,GAAG,CAAC,MAAmB,EAAE,MAAoB;QAClD,MAAM,GAAG,GAAW,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,eAAI,EAAC,6BAA6B,GAAG,iBAAiB,CAAC,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;QAClC,CAAC;aAAM,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7C,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,eAAI,EACF,6BAA6B,GAAG,gBAAgB,MAAM,CAAC,MAAM,mCAAmC,IAAI,CAAC,UAAU,EAAE,CAClH,CACF,CAAC;QACJ,CAAC;QACD,6EAA6E;QAC7E,0DAA0D;QAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACtE,6EAA6E;QAC7E,mCAAmC;QACnC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,wCAAwC;IACjC,MAAM,CAAC,MAAmB;QAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC,CAAC;QAC5C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,uCAAuC;IAChC,KAAK,CAAC,MAAoB,EAAE,IAAY;QAC7C,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC1C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,EAAE,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YACtC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,eAAI,EACF,iCAAiC,MAAM,CAAC,MAAM,mCAAmC,IAAI,CAAC,UAAU,EAAE,CACnG,CACF,CAAC;QACJ,CAAC;QACD,MAAM,cAAc,GAAW,mBAAmB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACtE,MAAM,IAAI,GAAsB,EAAE,CAAC;QACnC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,CAAC;gBACR,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC;aAC1E,CAAC,CAAC;QACL,CAAC;QACD,6EAA6E;QAC7E,wEAAwE;QACxE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACnF,CAAC;IAED;;;;;;;;;;;;OAYG;IACI,KAAK,CAAC,OAAO,CAAC,MAA2B,EAAE,KAAqB;QACrE,4EAA4E;QAC5E,wEAAwE;QACxE,YAAY;QACZ,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,MAAM,MAAM,GAA+C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC/E,IAAI,MAAM,CAAC,SAAS,EAAE,EAAE,CAAC;YACvB,OAAO,IAAA,eAAI,EAAC,iDAAiD,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAClC,MAAM,QAAQ,GAAyB,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAClE,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;gBACzB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,OAAO,IAAA,eAAI,EACT,oCAAoC,IAAA,qBAAa,EAAC,MAAM,CAAC,MAAM,CAAC,aAAa,QAAQ,CAAC,OAAO,EAAE,CAChG,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,GAAmB,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC5E,IAAI,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;gBACtB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,OAAO,IAAA,eAAI,EAAC,yBAAyB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QACD,OAAO,IAAA,kBAAO,EAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,4DAA4D;IACpD,MAAM;QACZ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,qDAAqD;IAC7C,MAAM,CAAC,UAAU,CAAC,MAAoB;QAC5C,IAAI,GAAG,GAAW,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAW,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/C,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACK,MAAM,CAAC,OAAO,CAAC,KAAmB,EAAE,cAAsB,EAAE,MAAoB;QACtF,MAAM,eAAe,GAAW,mBAAmB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACvE,IAAI,cAAc,KAAK,CAAC,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;YAClD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,GAAG,GAAW,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAW,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QACD,OAAO,GAAG,GAAG,CAAC,cAAc,GAAG,eAAe,CAAC,CAAC;IAClD,CAAC;CACF;AAvJD,kDAuJC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, edgeTargetKey } from '../types';\nimport {\n IMemoryRecordSource,\n IScopedMemoryRecord,\n IVectorIndex,\n IVectorQueryHit,\n MemoryEmbedder\n} from './vectorIndex';\n\n/** One stored embedding: the scope-qualified address plus its vector. */\ninterface IStoredVector {\n readonly target: IEdgeTarget;\n readonly vector: Float32Array;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n /**\n * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the\n * record's scope-qualified address, so two records that share a filename stem\n * across scopes occupy distinct entries and never overwrite each other.\n */\n private readonly _vectors: Map<string, IStoredVector>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<string, IStoredVector>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(key, { target, vector: Float32Array.from(vector) });\n // The in-memory index keys entries by the canonical scoped-target string, so\n // that key IS the entry reference.\n return Promise.resolve(succeed(key));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._vectors.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const stored of this._vectors.values()) {\n hits.push({\n target: stored.target,\n score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)\n });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns the number of vectors\n * indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty\n * rather than left in a partially-rebuilt state — a caller that retries a query\n * after a failed rebuild sees a clean empty index, never a half-populated one.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The embedder applied to each record.\n */\n public async rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder): Promise<Result<number>> {\n // Reset up front so the \"any failure leaves the index empty\" contract holds\n // even when the listing itself fails (no stale vectors survive a failed\n // rebuild).\n this._reset();\n const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await source.list();\n if (listed.isFailure()) {\n return fail(`vector index rebuild: failed to list records: ${listed.message}`);\n }\n for (const scoped of listed.value) {\n const embedded: Result<Float32Array> = await embed(scoped.record);\n if (embedded.isFailure()) {\n this._reset();\n return fail(\n `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${embedded.message}`\n );\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n this._reset();\n return fail(`vector index rebuild: ${added.message}`);\n }\n }\n return succeed(this._vectors.size);\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
@@ -1,14 +1,22 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { IMemoryRecord, MemoryId } from '../types';
2
+ import { IEdgeTarget, IMemoryRecord } from '../types';
3
3
  /**
4
- * A single hit returned by {@link IVectorIndex.query}: the matched record id and
5
- * the backend's similarity score (higher = more similar; the exact scale is
6
- * backend-defined). Hits are returned in descending score order.
4
+ * A single hit returned by {@link IVectorIndex.query}: the matched record's
5
+ * scope-qualified {@link IEdgeTarget | address} and the backend's similarity
6
+ * score (higher = more similar; the exact scale is backend-defined). Hits are
7
+ * returned in descending score order.
8
+ *
9
+ * @remarks
10
+ * The address is a `(scope, id)` pair, NOT a bare {@link MemoryId} — per-scope
11
+ * codecs (e.g. the medium-term codec's `turn-<n>` stems) legally mint the same
12
+ * stem under different scopes, so a bare id could not disambiguate two records
13
+ * that share a stem. The caller re-resolves the hit against the record index by
14
+ * the same scoped address.
7
15
  * @public
8
16
  */
9
17
  export interface IVectorQueryHit {
10
- /** The id of the matched record. */
11
- readonly id: MemoryId;
18
+ /** The scope-qualified address of the matched record. */
19
+ readonly target: IEdgeTarget;
12
20
  /** Backend similarity score; higher is more similar. */
13
21
  readonly score: number;
14
22
  }
@@ -29,16 +37,19 @@ export interface IVectorQueryHit {
29
37
  */
30
38
  export interface IVectorIndex {
31
39
  /**
32
- * Add (or replace) the embedding for `id`. Returns the opaque
33
- * {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps onto
34
- * the envelope so a later read knows the record is embedded.
40
+ * Add (or replace) the embedding for the scope-qualified `target`. Returns the
41
+ * opaque {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps
42
+ * onto the envelope so a later read knows the record is embedded. Keying on the
43
+ * `(scope, id)` address (not a bare id) is load-bearing: two records that share
44
+ * a filename stem across scopes must not clobber each other's embedding.
35
45
  */
36
- add(id: MemoryId, vector: Float32Array): Promise<Result<string>>;
46
+ add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
37
47
  /**
38
- * Remove the embedding for `id`. Returns the removed id. Idempotent — removing
39
- * an id with no embedding still succeeds (returns the id).
48
+ * Remove the embedding for the scope-qualified `target`. Returns the removed
49
+ * target. Idempotent — removing a target with no embedding still succeeds
50
+ * (returns the target).
40
51
  */
41
- remove(id: MemoryId): Promise<Result<MemoryId>>;
52
+ remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
42
53
  /**
43
54
  * Return the `topK` nearest records to `vector`, in descending score order.
44
55
  */
@@ -52,17 +63,32 @@ export interface IVectorIndex {
52
63
  * @public
53
64
  */
54
65
  export type MemoryEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<Float32Array>>;
66
+ /**
67
+ * A record paired with its scope-qualified {@link IEdgeTarget | address}, as
68
+ * yielded by {@link IMemoryRecordSource.list}. The address is required because
69
+ * {@link InMemoryCosineIndex.rebuild} keys each re-embedded entry on the
70
+ * scope-qualified target, not a bare {@link MemoryId} — two records that share a
71
+ * filename stem across scopes must not collide when the whole vault is re-indexed.
72
+ * @public
73
+ */
74
+ export interface IScopedMemoryRecord {
75
+ /** The record's scope-qualified `(scope, id)` address. */
76
+ readonly target: IEdgeTarget;
77
+ /** The record itself, passed to the embedder. */
78
+ readonly record: IMemoryRecord<unknown>;
79
+ }
55
80
  /**
56
81
  * The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to
57
- * re-embed an entire vault. {@link IMemoryStore} satisfies it structurally (its
58
- * `list` accepts an optional filter, which is assignable to this no-argument
59
- * shape), so a consumer passes the store directly without the vector packlet
60
- * taking a dependency on the store packlet (which depends on the vector packlet
61
- * for {@link IVectorIndex}, so the reverse import would be a cycle).
82
+ * re-embed an entire vault. Each entry carries the record's scope-qualified
83
+ * address (see {@link IScopedMemoryRecord}) so the rebuild keys the vector index
84
+ * exactly as the incremental embed-on-write path does. A consumer backs this with
85
+ * the store's scoped index the vector packlet does not import the store packlet
86
+ * (which depends on the vector packlet for {@link IVectorIndex}, so the reverse
87
+ * import would be a cycle).
62
88
  * @public
63
89
  */
64
90
  export interface IMemoryRecordSource {
65
- /** List every record in the vault. */
66
- list(): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
91
+ /** List every record in the vault, each paired with its scoped address. */
92
+ list(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
67
93
  }
68
94
  //# sourceMappingURL=vectorIndex.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vectorIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/vectorIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAEjE;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEhD;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;CAC5F;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;AAE/F;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,sCAAsC;IACtC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;CAChE"}
1
+ {"version":3,"file":"vectorIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/vectorIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEtD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAExE;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IAE1D;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;CAC5F;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;AAE/F;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;CAC7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"vectorIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/vectorIndex.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\n\n/**\n * A single hit returned by {@link IVectorIndex.query}: the matched record id and\n * the backend's similarity score (higher = more similar; the exact scale is\n * backend-defined). Hits are returned in descending score order.\n * @public\n */\nexport interface IVectorQueryHit {\n /** The id of the matched record. */\n readonly id: MemoryId;\n /** Backend similarity score; higher is more similar. */\n readonly score: number;\n}\n\n/**\n * The vector-index seam an embedding backend implements to make\n * {@link SemanticRetriever | semantic recall} operational.\n *\n * @remarks\n * Vectors cross this seam as `Float32Array` (the in-memory representation an\n * embedding model produces); `number[]` is reserved for the JSON-wire edges\n * (e.g. a provider's embedding response). The in-package brute-force cosine\n * implementation is {@link InMemoryCosineIndex}; a consumer can swap an external\n * ANN backend behind the same seam once N grows beyond the in-memory regime.\n *\n * Every operation returns a `Result` (async, since a real backend does I/O) so\n * failure is explicit and never throws across the seam.\n * @public\n */\nexport interface IVectorIndex {\n /**\n * Add (or replace) the embedding for `id`. Returns the opaque\n * {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps onto\n * the envelope so a later read knows the record is embedded.\n */\n add(id: MemoryId, vector: Float32Array): Promise<Result<string>>;\n\n /**\n * Remove the embedding for `id`. Returns the removed id. Idempotent — removing\n * an id with no embedding still succeeds (returns the id).\n */\n remove(id: MemoryId): Promise<Result<MemoryId>>;\n\n /**\n * Return the `topK` nearest records to `vector`, in descending score order.\n */\n query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;\n}\n\n/**\n * Embeds a complete record into a vector for the store's embed-on-write hook.\n * Async and `Result`-returning, since a real embedder does a network call (cloud\n * provider) or in-process model inference. The consumer wires this — the core\n * package never calls an embedding provider directly, staying embedder-agnostic.\n * @public\n */\nexport type MemoryEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<Float32Array>>;\n\n/**\n * The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to\n * re-embed an entire vault. {@link IMemoryStore} satisfies it structurally (its\n * `list` accepts an optional filter, which is assignable to this no-argument\n * shape), so a consumer passes the store directlywithout the vector packlet\n * taking a dependency on the store packlet (which depends on the vector packlet\n * for {@link IVectorIndex}, so the reverse import would be a cycle).\n * @public\n */\nexport interface IMemoryRecordSource {\n /** List every record in the vault. */\n list(): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n"]}
1
+ {"version":3,"file":"vectorIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/vectorIndex.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord } from '../types';\n\n/**\n * A single hit returned by {@link IVectorIndex.query}: the matched record's\n * scope-qualified {@link IEdgeTarget | address} and the backend's similarity\n * score (higher = more similar; the exact scale is backend-defined). Hits are\n * returned in descending score order.\n *\n * @remarks\n * The address is a `(scope, id)` pair, NOT a bare {@link MemoryId} — per-scope\n * codecs (e.g. the medium-term codec's `turn-<n>` stems) legally mint the same\n * stem under different scopes, so a bare id could not disambiguate two records\n * that share a stem. The caller re-resolves the hit against the record index by\n * the same scoped address.\n * @public\n */\nexport interface IVectorQueryHit {\n /** The scope-qualified address of the matched record. */\n readonly target: IEdgeTarget;\n /** Backend similarity score; higher is more similar. */\n readonly score: number;\n}\n\n/**\n * The vector-index seam an embedding backend implements to make\n * {@link SemanticRetriever | semantic recall} operational.\n *\n * @remarks\n * Vectors cross this seam as `Float32Array` (the in-memory representation an\n * embedding model produces); `number[]` is reserved for the JSON-wire edges\n * (e.g. a provider's embedding response). The in-package brute-force cosine\n * implementation is {@link InMemoryCosineIndex}; a consumer can swap an external\n * ANN backend behind the same seam once N grows beyond the in-memory regime.\n *\n * Every operation returns a `Result` (async, since a real backend does I/O) so\n * failure is explicit and never throws across the seam.\n * @public\n */\nexport interface IVectorIndex {\n /**\n * Add (or replace) the embedding for the scope-qualified `target`. Returns the\n * opaque {@link IMemoryEnvelope.embeddingRef | embeddingRef} the store stamps\n * onto the envelope so a later read knows the record is embedded. Keying on the\n * `(scope, id)` address (not a bare id) is load-bearing: two records that share\n * a filename stem across scopes must not clobber each other's embedding.\n */\n add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;\n\n /**\n * Remove the embedding for the scope-qualified `target`. Returns the removed\n * target. Idempotent — removing a target with no embedding still succeeds\n * (returns the target).\n */\n remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;\n\n /**\n * Return the `topK` nearest records to `vector`, in descending score order.\n */\n query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;\n}\n\n/**\n * Embeds a complete record into a vector for the store's embed-on-write hook.\n * Async and `Result`-returning, since a real embedder does a network call (cloud\n * provider) or in-process model inference. The consumer wires this — the core\n * package never calls an embedding provider directly, staying embedder-agnostic.\n * @public\n */\nexport type MemoryEmbedder = (record: IMemoryRecord<unknown>) => Promise<Result<Float32Array>>;\n\n/**\n * A record paired with its scope-qualified {@link IEdgeTarget | address}, as\n * yielded by {@link IMemoryRecordSource.list}. The address is required because\n * {@link InMemoryCosineIndex.rebuild} keys each re-embedded entry on the\n * scope-qualified target, not a bare {@link MemoryId} — two records that share a\n * filename stem across scopes must not collide when the whole vault is re-indexed.\n * @public\n */\nexport interface IScopedMemoryRecord {\n /** The record's scope-qualified `(scope, id)` address. */\n readonly target: IEdgeTarget;\n /** The record itself, passed to the embedder. */\n readonly record: IMemoryRecord<unknown>;\n}\n\n/**\n * The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to\n * re-embed an entire vault. Each entry carries the record's scope-qualified\n * address (see {@link IScopedMemoryRecord}) so the rebuild keys the vector index\n * exactly as the incremental embed-on-write path does. A consumer backs this with\n * the store's scoped index — the vector packlet does not import the store packlet\n * (which depends on the vector packlet for {@link IVectorIndex}, so the reverse\n * import would be a cycle).\n * @public\n */\nexport interface IMemoryRecordSource {\n /** List every record in the vault, each paired with its scoped address. */\n list(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;\n}\n"]}
@@ -37,7 +37,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
37
37
  links: [
38
38
  {
39
39
  type: 'rel',
40
- target: 'doc-2',
40
+ target: { scope: 'knowledge', id: 'doc-2' },
41
41
  confidence: 0.42,
42
42
  provenance: { source: 'agent', by: 'curator', extra: { nested: true } },
43
43
  valid_at: 111,
@@ -53,7 +53,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
53
53
  by: 'erik',
54
54
  model: 'gpt-5',
55
55
  confidence: 0.87,
56
- derivedFrom: 'turn-3',
56
+ derivedFrom: { scope: 'conversations/c1', id: 'turn-3' },
57
57
  // Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).
58
58
  sentiment: { score: 0.5 },
59
59
  epistemic: 'belief'
@@ -75,7 +75,7 @@ describe('antagonist — full on-disk round-trip preserves every optional field'
75
75
  entityId: 'doc-2',
76
76
  kind,
77
77
  tags: [],
78
- links: [{ type: 'rel', target: 'doc-3' }],
78
+ links: [{ type: 'rel', target: { scope: 'knowledge', id: 'doc-3' } }],
79
79
  created: 0,
80
80
  updated: 0,
81
81
  seq: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"antagonistRoundTrip.test.js","sourceRoot":"","sources":["../../../../src/test/unit/converters/antagonistRoundTrip.test.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAEH;;;;;;;GAOG;AAEH,8BAA4B;AAC5B,4CAA2C;AAC3C,0CAMwB;AAExB,MAAM,IAAI,GAAG,MAAiC,CAAC;AAE/C,SAAS,QAAQ;IACf,MAAM,GAAG,GAAG,6BAAqB,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;IACrD,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,qBAAU,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,QAAQ,CAAC,qEAAqE,EAAE,GAAG,EAAE;IACnF,4EAA4E;IAC5E,0EAA0E;IAC1E,wEAAwE;IACxE,+EAA+E;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,IAAI,CAAC,kGAAkG,EAAE,GAAG,EAAE;QAC5G,MAAM,QAAQ,GAAoB;YAChC,EAAE,EAAE,OAAgC;YACpC,QAAQ,EAAE,OAAsC;YAChD,IAAI;YACJ,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,CAAuC;YACxD,KAAK,EAAE;gBACL;oBACE,IAAI,EAAE,KAAc;oBACpB,MAAM,EAAE,OAAgB;oBACxB,UAAU,EAAE,IAAI;oBAChB,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;oBACvE,QAAQ,EAAE,GAAG;oBAEb,UAAU,EAAE,IAAI;iBACjB;aACF;YACD,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,GAAG,EAAE,CAAC;YACN,WAAW,EAAE,QAAQ;YACrB,UAAU,EAAE;gBACV,MAAM,EAAE,aAAa;gBACrB,EAAE,EAAE,MAAM;gBACV,KAAK,EAAE,OAAO;gBACd,UAAU,EAAE,IAAI;gBAChB,WAAW,EAAE,QAAiB;gBAC9B,0EAA0E;gBAC1E,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE;gBACzB,SAAS,EAAE,QAAQ;aACpB;YAED,QAAQ,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE;YAE7C,YAAY,EAAE,IAAI;SACnB,CAAC;QAEF,MAAM,GAAG,GAAG,IAAA,2BAAmB,EAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,OAAO,EAAE,CAAC;QACrE,MAAM,CAAC,IAAA,uBAAe,EAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE;YACtE,2EAA2E;YAC3E,mEAAmE;YACnE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yGAAyG,EAAE,GAAG,EAAE;QACnH,MAAM,QAAQ,GAAoB;YAChC,EAAE,EAAE,OAAgC;YACpC,QAAQ,EAAE,OAAsC;YAChD,IAAI;YACJ,IAAI,EAAE,EAAE;YACR,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAc,EAAE,MAAM,EAAE,OAAgB,EAAE,CAAC;YAC3D,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;YACV,GAAG,EAAE,CAAC;YACN,WAAW,EAAE,EAAE;YACf,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;SAChC,CAAC;QACF,MAAM,GAAG,GAAG,IAAA,2BAAmB,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5D,MAAM,CAAC,IAAA,uBAAe,EAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE;YACtE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,aAAa,EAAE,CAAC;YACrD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,aAAa,EAAE,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,aAAa,EAAE,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;YAC1D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,aAAa,EAAE,CAAC;YAC5D,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Antagonist torture test — convert/validate round-trip symmetry (target class\n * 7): every OPTIONAL field the envelope/edge/provenance shapes carry must\n * survive a full on-disk round-trip (serialize → YAML text → parse), not just a\n * single `Converter.convert` pass in memory. This exercises the exact path a\n * corrupted or field-dropping serializer/parser pair would break, mirroring the\n * `aiClientToolConfig`/`annotations` field-drop class named in the brief.\n */\n\nimport '@fgv/ts-utils-jest';\nimport { Converters } from '@fgv/ts-utils';\nimport {\n BodyConverterRegistry,\n IBodyConverterRegistry,\n IMemoryEnvelope,\n parseMemoryFile,\n serializeMemoryFile\n} from '../../../index';\n\nconst kind = 'note' as IMemoryEnvelope['kind'];\n\nfunction registry(): IBodyConverterRegistry {\n const reg = BodyConverterRegistry.create().orThrow();\n reg.register(kind, Converters.string);\n return reg;\n}\n\ndescribe('antagonist — full on-disk round-trip preserves every optional field', () => {\n // Wrong impl this catches: a serializer/parser pair where one side silently\n // drops an optional field (the exact class of bug the brief calls out for\n // `aiClientToolConfig`/`annotations`-shaped converters) — e.g. omitting\n // `temporal.invalid_at: null`, an edge's `valid_at`/`invalid_at`/`provenance`,\n // a `null` `embeddingRef`, or a provenance extension key, because the author\n // forgot to thread it through both the YAML emit AND the envelope Converter.\n test('every optional envelope/edge/provenance field set simultaneously survives a full YAML round-trip', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-1' as IMemoryEnvelope['id'],\n entityId: 'doc-1' as IMemoryEnvelope['entityId'],\n kind,\n tags: ['t1', 't2'] as unknown as IMemoryEnvelope['tags'],\n links: [\n {\n type: 'rel' as never,\n target: 'doc-2' as never,\n confidence: 0.42,\n provenance: { source: 'agent', by: 'curator', extra: { nested: true } },\n valid_at: 111,\n\n invalid_at: null\n }\n ],\n created: 1000,\n updated: 2000,\n seq: 7,\n contentHash: 'abc123',\n provenance: {\n source: 'host-ingest',\n by: 'erik',\n model: 'gpt-5',\n confidence: 0.87,\n derivedFrom: 'turn-3' as never,\n // Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).\n sentiment: { score: 0.5 },\n epistemic: 'belief'\n },\n\n temporal: { valid_at: 500, invalid_at: null },\n\n embeddingRef: null\n };\n\n const raw = serializeMemoryFile(envelope, 'the body text').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n // Deep-equal the ENTIRE envelope, not field-by-field — a partial assertion\n // list is exactly how a single dropped field slips through review.\n expect(record.envelope).toEqual(envelope);\n expect(record.body).toBe('the body text');\n });\n });\n\n test('an absent temporal/embeddingRef/edge-optionals round-trips to fully absent (no null-vs-undefined drift)', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-2' as IMemoryEnvelope['id'],\n entityId: 'doc-2' as IMemoryEnvelope['entityId'],\n kind,\n tags: [],\n links: [{ type: 'rel' as never, target: 'doc-3' as never }],\n created: 0,\n updated: 0,\n seq: 0,\n contentHash: '',\n provenance: { source: 'agent' }\n };\n const raw = serializeMemoryFile(envelope, 'body').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n expect(record.envelope.temporal).toBeUndefined();\n expect(record.envelope.embeddingRef).toBeUndefined();\n expect(record.envelope.links[0].confidence).toBeUndefined();\n expect(record.envelope.links[0].provenance).toBeUndefined();\n expect(record.envelope.links[0].valid_at).toBeUndefined();\n expect(record.envelope.links[0].invalid_at).toBeUndefined();\n expect(record.envelope).toEqual(envelope);\n });\n });\n});\n"]}
1
+ {"version":3,"file":"antagonistRoundTrip.test.js","sourceRoot":"","sources":["../../../../src/test/unit/converters/antagonistRoundTrip.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Antagonist torture test — convert/validate round-trip symmetry (target class\n * 7): every OPTIONAL field the envelope/edge/provenance shapes carry must\n * survive a full on-disk round-trip (serialize → YAML text → parse), not just a\n * single `Converter.convert` pass in memory. This exercises the exact path a\n * corrupted or field-dropping serializer/parser pair would break, mirroring the\n * `aiClientToolConfig`/`annotations` field-drop class named in the brief.\n */\n\nimport '@fgv/ts-utils-jest';\nimport { Converters } from '@fgv/ts-utils';\nimport {\n BodyConverterRegistry,\n IBodyConverterRegistry,\n IMemoryEnvelope,\n parseMemoryFile,\n serializeMemoryFile\n} from '../../../index';\n\nconst kind = 'note' as IMemoryEnvelope['kind'];\n\nfunction registry(): IBodyConverterRegistry {\n const reg = BodyConverterRegistry.create().orThrow();\n reg.register(kind, Converters.string);\n return reg;\n}\n\ndescribe('antagonist — full on-disk round-trip preserves every optional field', () => {\n // Wrong impl this catches: a serializer/parser pair where one side silently\n // drops an optional field (the exact class of bug the brief calls out for\n // `aiClientToolConfig`/`annotations`-shaped converters) — e.g. omitting\n // `temporal.invalid_at: null`, an edge's `valid_at`/`invalid_at`/`provenance`,\n // a `null` `embeddingRef`, or a provenance extension key, because the author\n // forgot to thread it through both the YAML emit AND the envelope Converter.\n test('every optional envelope/edge/provenance field set simultaneously survives a full YAML round-trip', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-1' as IMemoryEnvelope['id'],\n entityId: 'doc-1' as IMemoryEnvelope['entityId'],\n kind,\n tags: ['t1', 't2'] as unknown as IMemoryEnvelope['tags'],\n links: [\n {\n type: 'rel' as never,\n target: { scope: 'knowledge', id: 'doc-2' } as never,\n confidence: 0.42,\n provenance: { source: 'agent', by: 'curator', extra: { nested: true } },\n valid_at: 111,\n\n invalid_at: null\n }\n ],\n created: 1000,\n updated: 2000,\n seq: 7,\n contentHash: 'abc123',\n provenance: {\n source: 'host-ingest',\n by: 'erik',\n model: 'gpt-5',\n confidence: 0.87,\n derivedFrom: { scope: 'conversations/c1', id: 'turn-3' } as never,\n // Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).\n sentiment: { score: 0.5 },\n epistemic: 'belief'\n },\n\n temporal: { valid_at: 500, invalid_at: null },\n\n embeddingRef: null\n };\n\n const raw = serializeMemoryFile(envelope, 'the body text').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n // Deep-equal the ENTIRE envelope, not field-by-field — a partial assertion\n // list is exactly how a single dropped field slips through review.\n expect(record.envelope).toEqual(envelope);\n expect(record.body).toBe('the body text');\n });\n });\n\n test('an absent temporal/embeddingRef/edge-optionals round-trips to fully absent (no null-vs-undefined drift)', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-2' as IMemoryEnvelope['id'],\n entityId: 'doc-2' as IMemoryEnvelope['entityId'],\n kind,\n tags: [],\n links: [{ type: 'rel' as never, target: { scope: 'knowledge', id: 'doc-3' } as never }],\n created: 0,\n updated: 0,\n seq: 0,\n contentHash: '',\n provenance: { source: 'agent' }\n };\n const raw = serializeMemoryFile(envelope, 'body').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n expect(record.envelope.temporal).toBeUndefined();\n expect(record.envelope.embeddingRef).toBeUndefined();\n expect(record.envelope.links[0].confidence).toBeUndefined();\n expect(record.envelope.links[0].provenance).toBeUndefined();\n expect(record.envelope.links[0].valid_at).toBeUndefined();\n expect(record.envelope.links[0].invalid_at).toBeUndefined();\n expect(record.envelope).toEqual(envelope);\n });\n });\n});\n"]}
@@ -12,7 +12,7 @@ const validEnvelopeObject = {
12
12
  entityId: 'intro-to-rust',
13
13
  kind: 'knowledge',
14
14
  tags: ['rust', 'systems'],
15
- links: [{ type: 'related', target: 'ownership' }],
15
+ links: [{ type: 'related', target: { scope: 'knowledge', id: 'ownership' } }],
16
16
  created: 1000,
17
17
  updated: 2000,
18
18
  seq: 5,
@@ -33,6 +33,23 @@ describe('provenanceConverter', () => {
33
33
  test('accepts an arbitrary source string (open vocabulary)', () => {
34
34
  expect(converters_1.provenanceConverter.convert({ source: 'host-ingest' })).toSucceedWith({ source: 'host-ingest' });
35
35
  });
36
+ test('validates a scope-qualified derivedFrom (nested { scope, id })', () => {
37
+ expect(converters_1.provenanceConverter.convert({
38
+ source: 'host-ingest',
39
+ derivedFrom: { scope: 'conversations/conv-a', id: 'turn-3' }
40
+ })).toSucceedAndSatisfy((p) => {
41
+ expect(p.derivedFrom).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
42
+ });
43
+ });
44
+ test('rejects a bare-string derivedFrom (the pre-scoped format)', () => {
45
+ expect(converters_1.provenanceConverter.convert({ source: 'host-ingest', derivedFrom: 'turn-3' })).toFail();
46
+ });
47
+ test('rejects a derivedFrom missing scope', () => {
48
+ expect(converters_1.provenanceConverter.convert({ source: 'host-ingest', derivedFrom: { id: 'turn-3' } })).toFailWith(/scope/i);
49
+ });
50
+ test('rejects a derivedFrom missing id', () => {
51
+ expect(converters_1.provenanceConverter.convert({ source: 'host-ingest', derivedFrom: { scope: 'conversations/conv-a' } })).toFailWith(/id/i);
52
+ });
36
53
  test('fails when source is missing', () => {
37
54
  expect(converters_1.provenanceConverter.convert({ by: 'erik' })).toFail();
38
55
  });
@@ -40,18 +57,34 @@ describe('provenanceConverter', () => {
40
57
  expect(converters_1.provenanceConverter.convert('nope')).toFail();
41
58
  });
42
59
  });
60
+ describe('edgeTargetConverter', () => {
61
+ test('validates a scope-qualified target', () => {
62
+ expect(converters_1.edgeTargetConverter.convert({ scope: 'conversations/conv-a', id: 'turn-3' })).toSucceedAndSatisfy((t) => {
63
+ expect(t).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
64
+ });
65
+ });
66
+ test('rejects a target missing scope', () => {
67
+ expect(converters_1.edgeTargetConverter.convert({ id: 'turn-3' })).toFailWith(/scope/i);
68
+ });
69
+ test('rejects a target missing id', () => {
70
+ expect(converters_1.edgeTargetConverter.convert({ scope: 'conversations/conv-a' })).toFailWith(/id/i);
71
+ });
72
+ test('rejects a bare-string target (the pre-scoped format)', () => {
73
+ expect(converters_1.edgeTargetConverter.convert('turn-3')).toFail();
74
+ });
75
+ });
43
76
  describe('edgeConverter', () => {
44
77
  test('validates a full attributed edge', () => {
45
78
  expect(converters_1.edgeConverter.convert({
46
79
  type: 'mtm-ref',
47
- target: 'turn-3',
80
+ target: { scope: 'conversations/conv-a', id: 'turn-3' },
48
81
  confidence: 0.8,
49
82
  provenance: { source: 'agent' },
50
83
  valid_at: 100,
51
84
  invalid_at: 200
52
85
  })).toSucceedAndSatisfy((edge) => {
53
86
  expect(edge.type).toBe('mtm-ref');
54
- expect(edge.target).toBe('turn-3');
87
+ expect(edge.target).toEqual({ scope: 'conversations/conv-a', id: 'turn-3' });
55
88
  expect(edge.confidence).toBe(0.8);
56
89
  expect(edge.provenance).toEqual({ source: 'agent' });
57
90
  expect(edge.valid_at).toBe(100);
@@ -59,23 +92,26 @@ describe('edgeConverter', () => {
59
92
  });
60
93
  });
61
94
  test('validates a minimal edge', () => {
62
- expect(converters_1.edgeConverter.convert({ type: 'related', target: 'ownership' })).toSucceedAndSatisfy((edge) => {
95
+ expect(converters_1.edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'ownership' } })).toSucceedAndSatisfy((edge) => {
63
96
  expect(edge.type).toBe('related');
64
- expect(edge.target).toBe('ownership');
97
+ expect(edge.target).toEqual({ scope: 'knowledge', id: 'ownership' });
65
98
  expect(edge.confidence).toBeUndefined();
66
99
  });
67
100
  });
68
101
  test('accepts a null invalid_at (still valid)', () => {
69
- expect(converters_1.edgeConverter.convert({ type: 'related', target: 'x', invalid_at: null })).toSucceedAndSatisfy((edge) => {
102
+ expect(converters_1.edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'x' }, invalid_at: null })).toSucceedAndSatisfy((edge) => {
70
103
  expect(edge.invalid_at).toBeNull();
71
104
  });
72
105
  });
73
106
  test('fails when invalid_at is neither a number nor null', () => {
74
- expect(converters_1.edgeConverter.convert({ type: 'related', target: 'x', invalid_at: 'soon' })).toFail();
107
+ expect(converters_1.edgeConverter.convert({ type: 'related', target: { scope: 'knowledge', id: 'x' }, invalid_at: 'soon' })).toFail();
75
108
  });
76
109
  test('fails when target is missing', () => {
77
110
  expect(converters_1.edgeConverter.convert({ type: 'related' })).toFail();
78
111
  });
112
+ test('fails when target is a bare string (rejects the pre-scoped format)', () => {
113
+ expect(converters_1.edgeConverter.convert({ type: 'related', target: 'ownership' })).toFail();
114
+ });
79
115
  });
80
116
  describe('temporalConverter', () => {
81
117
  test('validates a temporal block', () => {
@@ -112,6 +148,28 @@ describe('envelopeConverter', () => {
112
148
  test('fails when embeddingRef is neither a string nor null', () => {
113
149
  expect(converters_1.envelopeConverter.convert(Object.assign(Object.assign({}, validEnvelopeObject), { embeddingRef: 42 }))).toFail();
114
150
  });
151
+ test('rank is absent when not present', () => {
152
+ expect(converters_1.envelopeConverter.convert(validEnvelopeObject)).toSucceedAndSatisfy((envelope) => {
153
+ expect(envelope.rank).toBeUndefined();
154
+ });
155
+ });
156
+ test('validates and preserves a numeric rank', () => {
157
+ expect(converters_1.envelopeConverter.convert(Object.assign(Object.assign({}, validEnvelopeObject), { rank: 42 }))).toSucceedAndSatisfy((envelope) => {
158
+ expect(envelope.rank).toBe(42);
159
+ });
160
+ });
161
+ test('serialize/parse round-trips rank through frontmatter', () => {
162
+ const registry = converters_1.BodyConverterRegistry.create().orThrow();
163
+ registry.register('knowledge', ts_utils_1.Converters.string);
164
+ const envelope = converters_1.envelopeConverter.convert(Object.assign(Object.assign({}, validEnvelopeObject), { rank: 7 })).orThrow();
165
+ const file = (0, converters_1.serializeMemoryFile)(envelope, 'body').orThrow();
166
+ expect((0, converters_1.parseMemoryFile)(file, registry)).toSucceedAndSatisfy((record) => {
167
+ expect(record.envelope.rank).toBe(7);
168
+ });
169
+ });
170
+ test('fails when rank is not a number', () => {
171
+ expect(converters_1.envelopeConverter.convert(Object.assign(Object.assign({}, validEnvelopeObject), { rank: 'high' }))).toFail();
172
+ });
115
173
  test('fails when a required field is missing', () => {
116
174
  const missing = Object.assign({}, validEnvelopeObject);
117
175
  delete missing.contentHash;
@@ -178,6 +236,65 @@ describe('parseMemoryFile / serializeMemoryFile', () => {
178
236
  expect(record.body).toBe('The knowledge body.');
179
237
  });
180
238
  });
239
+ test('round-trips a scope-qualified edge target through the frontmatter intact', () => {
240
+ const envelope = converters_1.envelopeConverter
241
+ .convert(Object.assign(Object.assign({}, validEnvelopeObject), { links: [{ type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }] }))
242
+ .orThrow();
243
+ const file = (0, converters_1.serializeMemoryFile)(envelope, 'body').orThrow();
244
+ // On-wire the target is a nested object, NOT a bare scalar.
245
+ expect(file).toMatch(/scope: conversations\/conv-a/);
246
+ expect(file).toMatch(/id: turn-3/);
247
+ expect((0, converters_1.parseMemoryFile)(file, registry)).toSucceedAndSatisfy((record) => {
248
+ expect(record.envelope.links).toEqual([
249
+ { type: 'mtm-ref', target: { scope: 'conversations/conv-a', id: 'turn-3' } }
250
+ ]);
251
+ // Serializing the reparsed record reproduces byte-identical output.
252
+ expect((0, converters_1.serializeMemoryFile)(record.envelope, record.body)).toSucceedWith(file);
253
+ });
254
+ });
255
+ test('round-trips a scope-qualified provenance.derivedFrom through the frontmatter intact', () => {
256
+ const envelope = converters_1.envelopeConverter
257
+ .convert(Object.assign(Object.assign({}, validEnvelopeObject), { provenance: {
258
+ source: 'host-ingest',
259
+ derivedFrom: { scope: 'conversations/conv-a', id: 'turn-3' }
260
+ } }))
261
+ .orThrow();
262
+ const file = (0, converters_1.serializeMemoryFile)(envelope, 'body').orThrow();
263
+ // On-wire the back-reference is a nested object, NOT a bare scalar.
264
+ expect(file).toMatch(/scope: conversations\/conv-a/);
265
+ expect((0, converters_1.parseMemoryFile)(file, registry)).toSucceedAndSatisfy((record) => {
266
+ expect(record.envelope.provenance.derivedFrom).toEqual({
267
+ scope: 'conversations/conv-a',
268
+ id: 'turn-3'
269
+ });
270
+ expect((0, converters_1.serializeMemoryFile)(record.envelope, record.body)).toSucceedWith(file);
271
+ });
272
+ });
273
+ test('two records whose derivedFrom share a stem across scopes round-trip as distinct scoped refs', () => {
274
+ // Same idStem ('turn-3'), different scopes — exactly the ambiguity a bare
275
+ // MemoryId could not disambiguate. Each must round-trip to ITS OWN scope.
276
+ const makeFile = (scope) => {
277
+ const envelope = converters_1.envelopeConverter
278
+ .convert(Object.assign(Object.assign({}, validEnvelopeObject), { provenance: { source: 'host-ingest', derivedFrom: { scope, id: 'turn-3' } } }))
279
+ .orThrow();
280
+ return (0, converters_1.serializeMemoryFile)(envelope, 'body').orThrow();
281
+ };
282
+ const fileA = makeFile('conversations/conv-a');
283
+ const fileB = makeFile('conversations/conv-b');
284
+ expect(fileA).not.toBe(fileB);
285
+ expect((0, converters_1.parseMemoryFile)(fileA, registry)).toSucceedAndSatisfy((record) => {
286
+ expect(record.envelope.provenance.derivedFrom).toEqual({
287
+ scope: 'conversations/conv-a',
288
+ id: 'turn-3'
289
+ });
290
+ });
291
+ expect((0, converters_1.parseMemoryFile)(fileB, registry)).toSucceedAndSatisfy((record) => {
292
+ expect(record.envelope.provenance.derivedFrom).toEqual({
293
+ scope: 'conversations/conv-b',
294
+ id: 'turn-3'
295
+ });
296
+ });
297
+ });
181
298
  test('fails when the frontmatter cannot be split', () => {
182
299
  expect((0, converters_1.parseMemoryFile)('no delimiters', registry)).toFailWith(/missing opening frontmatter delimiter/i);
183
300
  });