@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
@@ -7,22 +7,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  require("@fgv/ts-utils-jest");
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
9
  const index_1 = require("../../../index");
10
+ /** A scope-qualified link-traversal seed for the retriever tests. */
11
+ function et(id, scope = 'knowledge') {
12
+ return { scope: scope, id: id };
13
+ }
10
14
  function makeEntry(spec) {
11
15
  var _a, _b, _c, _d, _e;
12
16
  const record = {
13
17
  envelope: index_1.envelopeConverter
14
- .convert({
15
- id: spec.id,
16
- entityId: spec.id,
17
- kind: (_a = spec.kind) !== null && _a !== void 0 ? _a : 'knowledge',
18
- tags: (_b = spec.tags) !== null && _b !== void 0 ? _b : [],
19
- links: [],
20
- created: 0,
21
- updated: (_c = spec.updated) !== null && _c !== void 0 ? _c : 0,
22
- seq: (_d = spec.seq) !== null && _d !== void 0 ? _d : 0,
23
- contentHash: 'h',
24
- provenance: { source: 'agent' }
25
- })
18
+ .convert(Object.assign(Object.assign({ id: spec.id, entityId: spec.id, kind: (_a = spec.kind) !== null && _a !== void 0 ? _a : 'knowledge', tags: (_b = spec.tags) !== null && _b !== void 0 ? _b : [], links: [], created: 0, updated: (_c = spec.updated) !== null && _c !== void 0 ? _c : 0, seq: (_d = spec.seq) !== null && _d !== void 0 ? _d : 0, contentHash: 'h' }, (spec.rank !== undefined ? { rank: spec.rank } : {})), { provenance: { source: 'agent' } }))
26
19
  .orThrow(),
27
20
  body: `body-${spec.id}`
28
21
  };
@@ -57,12 +50,12 @@ describe('retrieve helpers', () => {
57
50
  expect((0, index_1.guardRetrieverCapabilities)({ asOf: 5, kind: knowledge }, noCaps)).toFailWith(/none configured for kind knowledge/i);
58
51
  });
59
52
  test('fails loudly when a link-traversal axis is requested but unsupported', () => {
60
- expect((0, index_1.guardRetrieverCapabilities)({ linkedTo: 'x' }, noCaps)).toFailWith(/link traversal requires a backlink index; none configured/i);
61
- expect((0, index_1.guardRetrieverCapabilities)({ linkedFrom: 'x' }, noCaps)).toFailWith(/link traversal requires/i);
53
+ expect((0, index_1.guardRetrieverCapabilities)({ linkedTo: et('x') }, noCaps)).toFailWith(/link traversal requires a backlink index; none configured/i);
54
+ expect((0, index_1.guardRetrieverCapabilities)({ linkedFrom: et('x') }, noCaps)).toFailWith(/link traversal requires/i);
62
55
  expect((0, index_1.guardRetrieverCapabilities)({ hops: 2 }, noCaps)).toFailWith(/link traversal requires/i);
63
56
  });
64
57
  test('passes a link-traversal axis when the capability is supported', () => {
65
- expect((0, index_1.guardRetrieverCapabilities)({ linkedTo: 'x' }, Object.assign(Object.assign({}, noCaps), { supportsLinkTraversal: true }))).toSucceed();
58
+ expect((0, index_1.guardRetrieverCapabilities)({ linkedTo: et('x') }, Object.assign(Object.assign({}, noCaps), { supportsLinkTraversal: true }))).toSucceed();
66
59
  });
67
60
  });
68
61
  describe('limitRecords', () => {
@@ -79,6 +72,25 @@ describe('retrieve helpers', () => {
79
72
  expect((0, index_1.limitRecords)(records, 0)).toEqual([]);
80
73
  expect((0, index_1.limitRecords)(records, -1)).toEqual([]);
81
74
  });
75
+ test('an absent offset is a no-op (byte-identical to today) — same reference returned', () => {
76
+ expect((0, index_1.limitRecords)(records, undefined, undefined)).toBe(records);
77
+ expect((0, index_1.limitRecords)(records, 2, undefined)).toHaveLength(2);
78
+ // A non-positive offset is guarded like a non-positive limit — no skip.
79
+ expect((0, index_1.limitRecords)(records, undefined, 0)).toBe(records);
80
+ expect(ids((0, index_1.limitRecords)(records, undefined, -5))).toEqual(['a', 'b', 'c']);
81
+ });
82
+ test('offset skips after ordering; { offset, limit } is a stable page window', () => {
83
+ expect(ids((0, index_1.limitRecords)(records, undefined, 1))).toEqual(['b', 'c']);
84
+ expect(ids((0, index_1.limitRecords)(records, 1, 1))).toEqual(['b']);
85
+ expect(ids((0, index_1.limitRecords)(records, 2, 1))).toEqual(['b', 'c']);
86
+ });
87
+ test('an offset past the end yields an empty page (never a throw)', () => {
88
+ expect((0, index_1.limitRecords)(records, undefined, 3)).toEqual([]);
89
+ expect((0, index_1.limitRecords)(records, 5, 99)).toEqual([]);
90
+ });
91
+ test('a non-positive limit still wins over an offset window', () => {
92
+ expect((0, index_1.limitRecords)(records, 0, 1)).toEqual([]);
93
+ });
82
94
  });
83
95
  });
84
96
  describe('RecencyRetriever', () => {
@@ -144,7 +156,200 @@ describe('RecencyRetriever', () => {
144
156
  });
145
157
  test('degrades loudly on a link-traversal request', async () => {
146
158
  const r = index_1.RecencyRetriever.create(buildIndex([{ id: 'a' }])).orThrow();
147
- expect(await r.retrieve({ linkedTo: 'a' })).toFailWith(/link traversal requires/i);
159
+ expect(await r.retrieve({ linkedTo: et('a') })).toFailWith(/link traversal requires/i);
160
+ });
161
+ });
162
+ describe('orderBy: rank axis', () => {
163
+ const a1 = makeEntry({ id: 'a', rank: 1, updated: 100, seq: 1 }).record;
164
+ const b3 = makeEntry({ id: 'b', rank: 3, updated: 100, seq: 2 }).record;
165
+ const c2 = makeEntry({ id: 'c', rank: 2, updated: 100, seq: 3 }).record;
166
+ const unranked = makeEntry({ id: 'z', updated: 999, seq: 4 }).record;
167
+ describe('rankCompare', () => {
168
+ test('orders by rank descending', () => {
169
+ expect([a1, b3, c2]
170
+ .slice()
171
+ .sort(index_1.rankCompare)
172
+ .map((r) => r.envelope.id)).toEqual(['b', 'c', 'a']);
173
+ });
174
+ test('places an absent-rank record last regardless of recency', () => {
175
+ // `unranked` has the newest `updated`, but an absent rank sorts after every ranked record.
176
+ expect([unranked, a1, b3]
177
+ .slice()
178
+ .sort(index_1.rankCompare)
179
+ .map((r) => r.envelope.id)).toEqual(['b', 'a', 'z']);
180
+ // Symmetric: absent on the left side of the compare too.
181
+ expect([a1, unranked]
182
+ .slice()
183
+ .sort(index_1.rankCompare)
184
+ .map((r) => r.envelope.id)).toEqual(['a', 'z']);
185
+ });
186
+ test('breaks an equal-rank tie by recency', () => {
187
+ const b3b = makeEntry({ id: 'b2', rank: 3, updated: 50, seq: 9 }).record;
188
+ // Equal rank 3 → newer `updated` (b3 at 100) first.
189
+ expect([b3b, b3]
190
+ .slice()
191
+ .sort(index_1.rankCompare)
192
+ .map((r) => r.envelope.id)).toEqual(['b', 'b2']);
193
+ });
194
+ });
195
+ describe('orderingCompare', () => {
196
+ test("returns recencyCompare for absent orderBy and 'recency'", () => {
197
+ expect((0, index_1.orderingCompare)()).toBe(index_1.recencyCompare);
198
+ expect((0, index_1.orderingCompare)('recency')).toBe(index_1.recencyCompare);
199
+ });
200
+ test("returns rankCompare for 'rank'", () => {
201
+ expect((0, index_1.orderingCompare)('rank')).toBe(index_1.rankCompare);
202
+ });
203
+ });
204
+ describe.each([
205
+ ['RecencyRetriever', (index) => index_1.RecencyRetriever.create(index).orThrow()],
206
+ ['TagRetriever', (index) => index_1.TagRetriever.create(index).orThrow()],
207
+ [
208
+ 'StructuredFilterRetriever',
209
+ (index) => index_1.StructuredFilterRetriever.create(index).orThrow()
210
+ ]
211
+ ])('%s honors orderBy', (name, make) => {
212
+ // Tag / structured-filter need their axis present to return anything.
213
+ const axis = name === 'TagRetriever'
214
+ ? { tag: 'x' }
215
+ : name === 'StructuredFilterRetriever'
216
+ ? { filter: () => true }
217
+ : {};
218
+ function orderedIndex() {
219
+ return buildIndex([
220
+ { id: 'a', rank: 1, tags: ['x'], updated: 100, seq: 1 },
221
+ { id: 'b', rank: 3, tags: ['x'], updated: 100, seq: 2 },
222
+ { id: 'c', rank: 2, tags: ['x'], updated: 100, seq: 3 },
223
+ { id: 'z', tags: ['x'], updated: 999, seq: 4 }
224
+ ]);
225
+ }
226
+ test("orderBy 'rank' orders by rank descending, absent last", async () => {
227
+ const r = make(orderedIndex());
228
+ expect(await r.retrieve(Object.assign(Object.assign({}, axis), { orderBy: 'rank' }))).toSucceedAndSatisfy((records) => {
229
+ expect(ids(records)).toEqual(['b', 'c', 'a', 'z']);
230
+ });
231
+ });
232
+ test('orderBy absent is unchanged recency order', async () => {
233
+ const r = make(orderedIndex());
234
+ expect(await r.retrieve(Object.assign({}, axis))).toSucceedAndSatisfy((records) => {
235
+ // `z` has the newest `updated`; the rest tie on `updated` and break by
236
+ // seq descending (c=3, b=2, a=1) — recency order regardless of rank.
237
+ expect(ids(records)).toEqual(['z', 'c', 'b', 'a']);
238
+ });
239
+ });
240
+ test('rank ordering composes with { limit, offset } for a bounded top-M page', async () => {
241
+ const r = make(orderedIndex());
242
+ expect(await r.retrieve(Object.assign(Object.assign({}, axis), { orderBy: 'rank', limit: 1, offset: 1 }))).toSucceedAndSatisfy((records) => {
243
+ expect(ids(records)).toEqual(['c']);
244
+ });
245
+ });
246
+ });
247
+ test("HybridRetriever re-orders the merged set by rank for orderBy 'rank'", async () => {
248
+ const index = buildIndex([
249
+ { id: 'a', rank: 1, tags: ['x'], updated: 100, seq: 1 },
250
+ { id: 'b', rank: 3, tags: ['x'], updated: 100, seq: 2 },
251
+ { id: 'c', rank: 2, tags: ['x'], updated: 100, seq: 3 }
252
+ ]);
253
+ const hybrid = index_1.HybridRetriever.create([index_1.RecencyRetriever.create(index).orThrow(), index_1.TagRetriever.create(index).orThrow()], index_1.ScoreUnionMergeStrategy.create().orThrow()).orThrow();
254
+ expect(await hybrid.retrieve({ tag: 'x', orderBy: 'rank' })).toSucceedAndSatisfy((records) => {
255
+ // All three score 2 (surfaced by both children); orderBy rank re-sorts the merge.
256
+ expect(ids(records)).toEqual(['b', 'c', 'a']);
257
+ });
258
+ });
259
+ test("SemanticRetriever preserves vector-similarity order regardless of orderBy 'rank'", async () => {
260
+ const index = buildIndex([
261
+ { id: 'a', rank: 9, updated: 1 },
262
+ { id: 'b', rank: 1, updated: 1 }
263
+ ]);
264
+ const vectorIndex = {
265
+ add: (t) => Promise.resolve((0, ts_utils_1.succeed)(`ref-${t.id}`)),
266
+ remove: (t) => Promise.resolve((0, ts_utils_1.succeed)(t)),
267
+ query: () => Promise.resolve((0, ts_utils_1.succeed)([
268
+ { target: et('b'), score: 0.9 },
269
+ { target: et('a'), score: 0.5 }
270
+ ]))
271
+ };
272
+ const r = index_1.SemanticRetriever.create({
273
+ index,
274
+ backend: {
275
+ vectorIndex,
276
+ embedQuery: () => Promise.resolve((0, ts_utils_1.succeed)(Float32Array.from([0.1])))
277
+ }
278
+ }).orThrow();
279
+ // Even though `a` has the higher rank, semantic keeps the similarity order (b before a).
280
+ expect(await r.retrieve({ semantic: 'q', orderBy: 'rank' })).toSucceedAndSatisfy((records) => {
281
+ expect(ids(records)).toEqual(['b', 'a']);
282
+ });
283
+ });
284
+ });
285
+ describe('query axes: kinds (kind-set) and offset', () => {
286
+ function multiKindRetriever() {
287
+ return index_1.RecencyRetriever.create(buildIndex([
288
+ { id: 'k1', kind: 'knowledge', updated: 40 },
289
+ { id: 'n1', kind: 'note', updated: 30 },
290
+ { id: 'e1', kind: 'event', updated: 20 },
291
+ { id: 'k2', kind: 'knowledge', updated: 10 }
292
+ ])).orThrow();
293
+ }
294
+ describe('kinds', () => {
295
+ test("an absent kinds axis imposes no kind constraint (today's behavior)", async () => {
296
+ expect(await multiKindRetriever().retrieve({})).toSucceedAndSatisfy((records) => {
297
+ expect([...ids(records)].sort()).toEqual(['e1', 'k1', 'k2', 'n1']);
298
+ });
299
+ });
300
+ test('restricts to records in ANY of the listed kinds', async () => {
301
+ expect(await multiKindRetriever().retrieve({
302
+ kinds: ['knowledge', 'event']
303
+ })).toSucceedAndSatisfy((records) => {
304
+ expect([...ids(records)].sort()).toEqual(['e1', 'k1', 'k2']);
305
+ });
306
+ });
307
+ test('an explicit empty kinds array matches NOTHING (not match-all)', async () => {
308
+ expect(await multiKindRetriever().retrieve({ kinds: [] })).toSucceedWith([]);
309
+ });
310
+ test('kind and kinds compose as AND (kind must be a member of kinds)', async () => {
311
+ // kind=knowledge AND kinds includes knowledge → knowledge records only.
312
+ expect(await multiKindRetriever().retrieve({
313
+ kind: knowledge,
314
+ kinds: ['knowledge', 'note']
315
+ })).toSucceedAndSatisfy((records) => {
316
+ expect([...ids(records)].sort()).toEqual(['k1', 'k2']);
317
+ });
318
+ // kind=knowledge but kinds excludes knowledge → nothing can satisfy both.
319
+ expect(await multiKindRetriever().retrieve({
320
+ kind: knowledge,
321
+ kinds: ['note', 'event']
322
+ })).toSucceedWith([]);
323
+ });
324
+ });
325
+ describe('offset', () => {
326
+ function ordered() {
327
+ return index_1.RecencyRetriever.create(buildIndex([
328
+ { id: 'a', updated: 10 },
329
+ { id: 'b', updated: 40 },
330
+ { id: 'c', updated: 30 },
331
+ { id: 'd', updated: 20 }
332
+ ])).orThrow();
333
+ }
334
+ test('skips after ordering, forming a stable { offset, limit } page window', async () => {
335
+ // Recency order is [b, c, d, a].
336
+ expect(await ordered().retrieve({ offset: 1, limit: 2 })).toSucceedAndSatisfy((records) => {
337
+ expect(ids(records)).toEqual(['c', 'd']);
338
+ });
339
+ });
340
+ test('an absent offset is unchanged from today', async () => {
341
+ expect(await ordered().retrieve({ limit: 2 })).toSucceedAndSatisfy((records) => {
342
+ expect(ids(records)).toEqual(['b', 'c']);
343
+ });
344
+ });
345
+ test('an offset past the end yields an empty page', async () => {
346
+ expect(await ordered().retrieve({ offset: 99 })).toSucceedWith([]);
347
+ });
348
+ test('a negative offset does not slip into slice (treated as no skip)', async () => {
349
+ expect(await ordered().retrieve({ offset: -3, limit: 2 })).toSucceedAndSatisfy((records) => {
350
+ expect(ids(records)).toEqual(['b', 'c']);
351
+ });
352
+ });
148
353
  });
149
354
  });
150
355
  /** A retriever with configurable capabilities that records the query it received. */
@@ -199,11 +404,11 @@ class FakeVectorIndex {
199
404
  this._hits = hits;
200
405
  this._failQuery = failQuery;
201
406
  }
202
- add(id) {
203
- return Promise.resolve((0, ts_utils_1.succeed)(`ref-${id}`));
407
+ add(t) {
408
+ return Promise.resolve((0, ts_utils_1.succeed)(`ref-${t.id}`));
204
409
  }
205
- remove(id) {
206
- return Promise.resolve((0, ts_utils_1.succeed)(id));
410
+ remove(t) {
411
+ return Promise.resolve((0, ts_utils_1.succeed)(t));
207
412
  }
208
413
  query(__vector, topK) {
209
414
  this.lastTopK = topK;
@@ -245,8 +450,8 @@ describe('SemanticRetriever', () => {
245
450
  index,
246
451
  backend: {
247
452
  vectorIndex: new FakeVectorIndex([
248
- { id: 'b', score: 0.9 },
249
- { id: 'a', score: 0.5 }
453
+ { target: et('b'), score: 0.9 },
454
+ { target: et('a'), score: 0.5 }
250
455
  ]),
251
456
  embedQuery: okEmbed
252
457
  }
@@ -265,9 +470,9 @@ describe('SemanticRetriever', () => {
265
470
  index,
266
471
  backend: {
267
472
  vectorIndex: new FakeVectorIndex([
268
- { id: 'gone', score: 0.99 },
269
- { id: 'b', score: 0.8 },
270
- { id: 'a', score: 0.7 }
473
+ { target: et('gone'), score: 0.99 },
474
+ { target: et('b'), score: 0.8 },
475
+ { target: et('a'), score: 0.7 }
271
476
  ]),
272
477
  embedQuery: okEmbed
273
478
  }
@@ -276,12 +481,37 @@ describe('SemanticRetriever', () => {
276
481
  expect(ids(records)).toEqual(['a']);
277
482
  });
278
483
  });
484
+ test('resolves a hit to the correctly-scoped record when two records share an id stem across scopes', async () => {
485
+ // Two records with the identical stem `turn-3` under different scopes — the
486
+ // exact same-stem-across-scopes collision the scope-qualified hit fixes. The
487
+ // retriever must re-resolve each hit through its full `(scope, id)` key, not a
488
+ // bare id (which would ambiguously match either record).
489
+ const index = buildIndex([
490
+ { id: 'turn-3', scope: 'conv-a', tags: ['from-a'] },
491
+ { id: 'turn-3', scope: 'conv-b', tags: ['from-b'] }
492
+ ]);
493
+ const r = index_1.SemanticRetriever.create({
494
+ index,
495
+ backend: {
496
+ // The vector backend scored the conv-b record; the retriever must return
497
+ // exactly that record, never the same-stem conv-a record.
498
+ vectorIndex: new FakeVectorIndex([{ target: et('turn-3', 'conv-b'), score: 0.9 }]),
499
+ embedQuery: okEmbed
500
+ }
501
+ }).orThrow();
502
+ expect(await r.retrieve({ semantic: 'q' })).toSucceedAndSatisfy((records) => {
503
+ expect(records).toHaveLength(1);
504
+ expect(records[0].envelope.id).toBe('turn-3');
505
+ // The tag proves it is the conv-b record, not the same-stem conv-a one.
506
+ expect(records[0].envelope.tags).toEqual(['from-b']);
507
+ });
508
+ });
279
509
  test('forwards topK to the vector index and applies the post-filter limit', async () => {
280
510
  const index = buildIndex([{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
281
511
  const vectorIndex = new FakeVectorIndex([
282
- { id: 'a', score: 0.9 },
283
- { id: 'b', score: 0.8 },
284
- { id: 'c', score: 0.7 }
512
+ { target: et('a'), score: 0.9 },
513
+ { target: et('b'), score: 0.8 },
514
+ { target: et('c'), score: 0.7 }
285
515
  ]);
286
516
  const r = index_1.SemanticRetriever.create({ index, backend: { vectorIndex, embedQuery: okEmbed } }).orThrow();
287
517
  expect(await r.retrieve({ semantic: 'q', topK: 2, limit: 2 })).toSucceedAndSatisfy((records) => {
@@ -291,7 +521,7 @@ describe('SemanticRetriever', () => {
291
521
  expect(vectorIndex.lastTopK).toBe(2);
292
522
  });
293
523
  test('defaults topK to 10 when the query omits it', async () => {
294
- const vectorIndex = new FakeVectorIndex([{ id: 'a', score: 0.9 }]);
524
+ const vectorIndex = new FakeVectorIndex([{ target: et('a'), score: 0.9 }]);
295
525
  const r = index_1.SemanticRetriever.create({
296
526
  index: buildIndex([{ id: 'a' }]),
297
527
  backend: { vectorIndex, embedQuery: okEmbed }
@@ -329,8 +559,8 @@ describe('SemanticRetriever', () => {
329
559
  test('normalizes a rejecting vector backend into a Failure', async () => {
330
560
  // A vector index whose `query` rejects (throws) rather than returning a fail.
331
561
  const rejectingIndex = {
332
- add: (id) => Promise.resolve((0, ts_utils_1.succeed)(`ref-${id}`)),
333
- remove: (id) => Promise.resolve((0, ts_utils_1.succeed)(id)),
562
+ add: (t) => Promise.resolve((0, ts_utils_1.succeed)(`ref-${t.id}`)),
563
+ remove: (t) => Promise.resolve((0, ts_utils_1.succeed)(t)),
334
564
  query: () => Promise.reject(new Error('socket hangup'))
335
565
  };
336
566
  const r = index_1.SemanticRetriever.create({
@@ -415,7 +645,7 @@ describe('HybridRetriever', () => {
415
645
  const semantic = index_1.SemanticRetriever.create({
416
646
  index,
417
647
  backend: {
418
- vectorIndex: new FakeVectorIndex([{ id: 'b', score: 0.9 }]),
648
+ vectorIndex: new FakeVectorIndex([{ target: et('b'), score: 0.9 }]),
419
649
  embedQuery: () => Promise.resolve((0, ts_utils_1.succeed)(Float32Array.from([1])))
420
650
  }
421
651
  }).orThrow();
@@ -442,9 +672,9 @@ describe('HybridRetriever', () => {
442
672
  const hybrid = index_1.HybridRetriever.create([linkChild, plainChild], index_1.ScoreUnionMergeStrategy.create().orThrow()).orThrow();
443
673
  expect(hybrid.capabilities.supportsLinkTraversal).toBe(true);
444
674
  // Union supports link traversal, so the hybrid does NOT loud-fail.
445
- expect(await hybrid.retrieve({ linkedTo: 'a', hops: 2 })).toSucceed();
675
+ expect(await hybrid.retrieve({ linkedTo: et('a'), hops: 2 })).toSucceed();
446
676
  // The link-capable child keeps the axes; the plain child has them stripped.
447
- expect((_a = linkChild.lastQuery) === null || _a === void 0 ? void 0 : _a.linkedTo).toBe('a');
677
+ expect((_a = linkChild.lastQuery) === null || _a === void 0 ? void 0 : _a.linkedTo).toEqual(et('a'));
448
678
  expect((_b = linkChild.lastQuery) === null || _b === void 0 ? void 0 : _b.hops).toBe(2);
449
679
  expect((_c = plainChild.lastQuery) === null || _c === void 0 ? void 0 : _c.linkedTo).toBeUndefined();
450
680
  expect((_d = plainChild.lastQuery) === null || _d === void 0 ? void 0 : _d.hops).toBeUndefined();
@@ -484,8 +714,8 @@ describe('HybridRetriever', () => {
484
714
  index,
485
715
  backend: {
486
716
  vectorIndex: new FakeVectorIndex([
487
- { id: 'd', score: 0.9 },
488
- { id: 'a', score: 0.8 }
717
+ { target: et('d'), score: 0.9 },
718
+ { target: et('a'), score: 0.8 }
489
719
  ]),
490
720
  embedQuery: () => Promise.resolve((0, ts_utils_1.succeed)(Float32Array.from([1])))
491
721
  }
@@ -504,5 +734,37 @@ describe('HybridRetriever', () => {
504
734
  expect(records[0].envelope.id).toBe('b');
505
735
  });
506
736
  });
737
+ test('applies the { offset, limit } window after merge', async () => {
738
+ const { index } = build();
739
+ const recency = index_1.RecencyRetriever.create(index).orThrow();
740
+ const hybrid = index_1.HybridRetriever.create([recency], index_1.ScoreUnionMergeStrategy.create().orThrow()).orThrow();
741
+ // Merged recency order is [b(30), c(20), a(10)]; offset 1 + limit 1 → [c].
742
+ expect(await hybrid.retrieve({ offset: 1, limit: 1 })).toSucceedAndSatisfy((records) => {
743
+ expect(ids(records)).toEqual(['c']);
744
+ });
745
+ });
746
+ test('strips offset from child queries (offset is a post-merge concern)', async () => {
747
+ var _a, _b;
748
+ const child = new RecordingRetriever({
749
+ supportsSemanticRecall: false,
750
+ supportsTemporalQuery: false,
751
+ supportsLinkTraversal: false
752
+ });
753
+ const hybrid = index_1.HybridRetriever.create([child], index_1.ScoreUnionMergeStrategy.create().orThrow()).orThrow();
754
+ expect(await hybrid.retrieve({ offset: 2, limit: 3 })).toSucceed();
755
+ expect((_a = child.lastQuery) === null || _a === void 0 ? void 0 : _a.offset).toBeUndefined();
756
+ expect((_b = child.lastQuery) === null || _b === void 0 ? void 0 : _b.limit).toBeUndefined();
757
+ });
758
+ test('passes the kinds axis through to children (a shared pre-filter axis)', async () => {
759
+ var _a;
760
+ const child = new RecordingRetriever({
761
+ supportsSemanticRecall: false,
762
+ supportsTemporalQuery: false,
763
+ supportsLinkTraversal: false
764
+ });
765
+ const hybrid = index_1.HybridRetriever.create([child], index_1.ScoreUnionMergeStrategy.create().orThrow()).orThrow();
766
+ expect(await hybrid.retrieve({ kinds: ['knowledge'] })).toSucceed();
767
+ expect((_a = child.lastQuery) === null || _a === void 0 ? void 0 : _a.kinds).toEqual(['knowledge']);
768
+ });
507
769
  });
508
770
  //# sourceMappingURL=retrievers.test.js.map