@fgv/ts-agent-memory 5.1.0-49 → 5.1.0-50

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 (164) hide show
  1. package/dist/packlets/index/memoryIndex.js +23 -16
  2. package/dist/packlets/index/memoryIndex.js.map +1 -1
  3. package/dist/packlets/ingest/orchestrator.js +13 -1
  4. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  5. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  6. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  8. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/retriever.js +91 -10
  10. package/dist/packlets/retrieve/retriever.js.map +1 -1
  11. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  12. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  13. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  14. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  16. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  18. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  19. package/dist/packlets/store/coverage.js +6 -0
  20. package/dist/packlets/store/coverage.js.map +1 -0
  21. package/dist/packlets/store/fileTreeMemoryStore.js +221 -79
  22. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  23. package/dist/packlets/store/index.js +4 -0
  24. package/dist/packlets/store/index.js.map +1 -1
  25. package/dist/packlets/store/listSelection.js +36 -0
  26. package/dist/packlets/store/listSelection.js.map +1 -0
  27. package/dist/packlets/store/memoryStore.js +6 -0
  28. package/dist/packlets/store/memoryStore.js.map +1 -0
  29. package/dist/packlets/store/reconcile.js +6 -0
  30. package/dist/packlets/store/reconcile.js.map +1 -0
  31. package/dist/packlets/store/storeCoverage.js +102 -0
  32. package/dist/packlets/store/storeCoverage.js.map +1 -0
  33. package/dist/packlets/store/storeReconcile.js +122 -0
  34. package/dist/packlets/store/storeReconcile.js.map +1 -0
  35. package/dist/packlets/store/vectorMaintenance.js +116 -8
  36. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  37. package/dist/packlets/store/vectorRecordSource.js +44 -0
  38. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  39. package/dist/packlets/tools/memoryTools.js +25 -2
  40. package/dist/packlets/tools/memoryTools.js.map +1 -1
  41. package/dist/packlets/types/envelope.js +25 -0
  42. package/dist/packlets/types/envelope.js.map +1 -1
  43. package/dist/packlets/types/index.js +1 -0
  44. package/dist/packlets/types/index.js.map +1 -1
  45. package/dist/packlets/types/recordResolver.js +6 -0
  46. package/dist/packlets/types/recordResolver.js.map +1 -0
  47. package/dist/packlets/types/temporal.js.map +1 -1
  48. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  49. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  50. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +67 -12
  51. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  52. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  53. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  54. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  55. package/dist/ts-agent-memory.d.ts +1035 -106
  56. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  57. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  58. package/lib/packlets/index/memoryIndex.js +23 -16
  59. package/lib/packlets/index/memoryIndex.js.map +1 -1
  60. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  61. package/lib/packlets/ingest/orchestrator.js +13 -1
  62. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  63. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  64. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  65. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  66. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  67. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  68. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  69. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  70. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  71. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  72. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  73. package/lib/packlets/retrieve/retriever.js +94 -9
  74. package/lib/packlets/retrieve/retriever.js.map +1 -1
  75. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  76. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  77. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  78. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  79. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  80. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  81. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  82. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  83. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  84. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  85. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  86. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  87. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  88. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  90. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  91. package/lib/packlets/store/coverage.d.ts +102 -0
  92. package/lib/packlets/store/coverage.d.ts.map +1 -0
  93. package/lib/packlets/store/coverage.js +7 -0
  94. package/lib/packlets/store/coverage.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +53 -166
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/store/index.d.ts +4 -0
  100. package/lib/packlets/store/index.d.ts.map +1 -1
  101. package/lib/packlets/store/index.js +4 -0
  102. package/lib/packlets/store/index.js.map +1 -1
  103. package/lib/packlets/store/listSelection.d.ts +101 -0
  104. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  105. package/lib/packlets/store/listSelection.js +40 -0
  106. package/lib/packlets/store/listSelection.js.map +1 -0
  107. package/lib/packlets/store/memoryStore.d.ts +237 -0
  108. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  109. package/lib/packlets/store/memoryStore.js +7 -0
  110. package/lib/packlets/store/memoryStore.js.map +1 -0
  111. package/lib/packlets/store/reconcile.d.ts +82 -0
  112. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  113. package/lib/packlets/store/reconcile.js +7 -0
  114. package/lib/packlets/store/reconcile.js.map +1 -0
  115. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  116. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  117. package/lib/packlets/store/storeCoverage.js +105 -0
  118. package/lib/packlets/store/storeCoverage.js.map +1 -0
  119. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  120. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  121. package/lib/packlets/store/storeReconcile.js +125 -0
  122. package/lib/packlets/store/storeReconcile.js.map +1 -0
  123. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  124. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  125. package/lib/packlets/store/vectorMaintenance.js +117 -8
  126. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  127. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  128. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  129. package/lib/packlets/store/vectorRecordSource.js +47 -0
  130. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  131. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  132. package/lib/packlets/tools/memoryTools.js +25 -2
  133. package/lib/packlets/tools/memoryTools.js.map +1 -1
  134. package/lib/packlets/types/envelope.d.ts +24 -2
  135. package/lib/packlets/types/envelope.d.ts.map +1 -1
  136. package/lib/packlets/types/envelope.js +26 -0
  137. package/lib/packlets/types/envelope.js.map +1 -1
  138. package/lib/packlets/types/index.d.ts +1 -0
  139. package/lib/packlets/types/index.d.ts.map +1 -1
  140. package/lib/packlets/types/index.js +1 -0
  141. package/lib/packlets/types/index.js.map +1 -1
  142. package/lib/packlets/types/recordResolver.d.ts +39 -0
  143. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  144. package/lib/packlets/types/recordResolver.js +7 -0
  145. package/lib/packlets/types/recordResolver.js.map +1 -0
  146. package/lib/packlets/types/temporal.d.ts +26 -6
  147. package/lib/packlets/types/temporal.d.ts.map +1 -1
  148. package/lib/packlets/types/temporal.js.map +1 -1
  149. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  150. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  151. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  152. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  153. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +6 -3
  154. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  155. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
  156. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  157. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  158. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  159. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  160. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  161. package/lib/packlets/vector/vectorIndex.d.ts +270 -15
  162. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  163. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  164. package/package.json +7 -7
@@ -1,6 +1,23 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';
3
- import { IIndexedMemoryRecord } from '../index';
2
+ import { IEdgeTarget, IEnvelopeCarrier, IMemoryRecord, IMemoryRecordResolver, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';
3
+ import { IIndexedMemoryEntry, IMemoryIndex } from '../index';
4
+ /**
5
+ * What every index-backed retriever needs: the index it selects over, and the
6
+ * resolver it materializes survivors through.
7
+ *
8
+ * @remarks
9
+ * The resolver is the whole of the partial-read migration for a caller that
10
+ * constructs retrievers directly — the index projection itself is invisible to
11
+ * anyone who only consumes `retrieve()`. `FileTreeMemoryStore` implements
12
+ * {@link IMemoryRecordResolver}, so wiring is `{ index, resolver: store }`.
13
+ * @public
14
+ */
15
+ export interface IRetrieverCreateParams {
16
+ /** The derived index to select over. */
17
+ readonly index: IMemoryIndex;
18
+ /** Materializes the selected entries' bodies. */
19
+ readonly resolver: IMemoryRecordResolver;
20
+ }
4
21
  /**
5
22
  * The capabilities a retriever exposes. A consumer probes these before
6
23
  * dispatching a query so it never silently gets an empty result for a
@@ -161,7 +178,7 @@ export declare function guardRetrieverCapabilities(query: IMemoryQuery, capabili
161
178
  * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.
162
179
  * @public
163
180
  */
164
- export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number;
181
+ export declare function recencyCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
165
182
  /**
166
183
  * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with
167
184
  * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST
@@ -169,14 +186,14 @@ export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryReco
169
186
  * index's rank-view ordering.
170
187
  * @public
171
188
  */
172
- export declare function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number;
189
+ export declare function rankCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
173
190
  /**
174
191
  * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}
175
192
  * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the
176
193
  * default, byte-identical to the pre-`orderBy` behavior).
177
194
  * @public
178
195
  */
179
- export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number;
196
+ export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IEnvelopeCarrier, b: IEnvelopeCarrier) => number;
180
197
  /**
181
198
  * Whether an indexed entry satisfies a query's scope / kind / tag /
182
199
  * provenance-source / predicate pre-filter (the axes shared by every v1
@@ -184,14 +201,78 @@ export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a:
184
201
  * are each retriever's own concern.
185
202
  * @public
186
203
  */
187
- export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean;
204
+ export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryEntry, query: IMemoryQuery): boolean;
188
205
  /**
189
206
  * Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
190
207
  * to a set of indexed entries, returning the surviving records (unordered,
191
208
  * unlimited).
192
209
  * @public
193
210
  */
194
- export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryRecord>, query: IMemoryQuery): IMemoryRecord<unknown>[];
211
+ export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryEntry>, query: IMemoryQuery): IIndexedMemoryEntry[];
212
+ /**
213
+ * The shared select → order → page → materialize pipeline every non-semantic
214
+ * retriever runs, and the one place the body-vs-envelope ordering decision lives.
215
+ *
216
+ * @public
217
+ * @remarks
218
+ * **Without `query.filter`, ordering and paging happen over envelopes and only
219
+ * the page is materialized** — so `limit` genuinely bounds the number of records
220
+ * read, not merely the number returned. That is what makes `limit` a legitimate
221
+ * narrowing axis rather than a loophole, and it is conditional on the ordering
222
+ * key being an envelope field: both shipped comparators (`recencyCompare`,
223
+ * `rankCompare`) read `updated` / `seq` / `rank` and qualify. **An ordering that
224
+ * keyed on a body field could not use this path** and would have to sort after
225
+ * materialization, at which point `limit` bounds the result and not the read.
226
+ *
227
+ * **With `query.filter`, every envelope-survivor must be materialized first**,
228
+ * because the predicate takes a whole record. Semantics are preserved exactly;
229
+ * the cost is not. Pair `filter` with an envelope axis when the read cost
230
+ * matters.
231
+ */
232
+ export declare function resolveQuery(entries: ReadonlyArray<IIndexedMemoryEntry>, query: IMemoryQuery, resolver: IMemoryRecordResolver): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
233
+ /**
234
+ * Materialize a selected, ordered page — **applying `query.filter` and paging in
235
+ * the right order**, which is the whole reason this is shared rather than
236
+ * open-coded per retriever.
237
+ *
238
+ * @remarks
239
+ * Two paths, and the choice is forced by where the predicate can run:
240
+ *
241
+ * - **No `filter`** — order and page over *envelopes*, then read only the page.
242
+ * `limit` bounds the READ, not just the result.
243
+ * - **With `filter`** — the predicate takes a whole record, so every
244
+ * envelope-survivor must be read first, then filtered, then paged. Paging
245
+ * before filtering would return fewer than `limit` rows for no reason a caller
246
+ * could see.
247
+ *
248
+ * **Every retriever must route through this.** `indexedRecordMatchesQuery`
249
+ * structurally *cannot* apply `filter` — it is handed an envelope — so a
250
+ * retriever that pre-filters with it and then materializes on its own silently
251
+ * ignores the predicate. That regression shipped once, in the stream that moved
252
+ * `filter` out of the pre-filter; this function exists so it cannot recur.
253
+ *
254
+ * @param selected - Entries surviving the envelope pre-filter.
255
+ * @param query - The query whose `filter` / `orderBy` / `limit` / `offset` apply.
256
+ * @param resolver - Body resolver.
257
+ * @param order - Applied to whichever collection is paged; identity is legal for
258
+ * a retriever whose ordering is intrinsic (semantic score, traversal order).
259
+ * @public
260
+ */
261
+ export declare function materializePage<T extends IIndexedMemoryEntry>(selected: ReadonlyArray<T>, query: IMemoryQuery, resolver: IMemoryRecordResolver, order?: (candidates: ReadonlyArray<T>) => ReadonlyArray<T>): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
262
+ /**
263
+ * Materialize entries through the resolver, dropping any that have vanished
264
+ * since selection — a concurrent delete between selecting an envelope and
265
+ * reading its body is a legitimate race and yields a shorter list, not an error.
266
+ * A read that FAILS is a real fault and propagates.
267
+ * @public
268
+ */
269
+ export declare function materializeEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, resolver: IMemoryRecordResolver): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
270
+ /**
271
+ * The {@link limitRecords} window, applied to entries — used on the
272
+ * no-body-filter path so paging happens before anything is read.
273
+ * @public
274
+ */
275
+ export declare function limitEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, limit?: number, offset?: number): ReadonlyArray<IIndexedMemoryEntry>;
195
276
  /**
196
277
  * Apply the `{ offset, limit }` page window to an ordered record set. Applied
197
278
  * last, after ordering, so it always takes a stable window of the ordered
@@ -1 +1 @@
1
- {"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,10 @@ exports.rankCompare = rankCompare;
12
12
  exports.orderingCompare = orderingCompare;
13
13
  exports.indexedRecordMatchesQuery = indexedRecordMatchesQuery;
14
14
  exports.selectByQuery = selectByQuery;
15
+ exports.resolveQuery = resolveQuery;
16
+ exports.materializePage = materializePage;
17
+ exports.materializeEntries = materializeEntries;
18
+ exports.limitEntries = limitEntries;
15
19
  exports.limitRecords = limitRecords;
16
20
  const ts_utils_1 = require("@fgv/ts-utils");
17
21
  /**
@@ -114,22 +118,21 @@ function indexedRecordMatchesQuery(entry, query) {
114
118
  if (query.scope !== undefined && entry.scope !== query.scope) {
115
119
  return false;
116
120
  }
117
- if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {
121
+ if (query.kind !== undefined && entry.envelope.kind !== query.kind) {
118
122
  return false;
119
123
  }
120
- if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {
124
+ if (query.kinds !== undefined && !query.kinds.includes(entry.envelope.kind)) {
121
125
  return false;
122
126
  }
123
- if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
127
+ if (query.tag !== undefined && !entry.envelope.tags.includes(query.tag)) {
124
128
  return false;
125
129
  }
126
- if (query.provenanceSource !== undefined &&
127
- entry.record.envelope.provenance.source !== query.provenanceSource) {
128
- return false;
129
- }
130
- if (query.filter !== undefined && !query.filter(entry.record)) {
130
+ if (query.provenanceSource !== undefined && entry.envelope.provenance.source !== query.provenanceSource) {
131
131
  return false;
132
132
  }
133
+ // `query.filter` is deliberately NOT applied here — it is a predicate over the
134
+ // whole record, and the index holds envelopes. It runs after materialization;
135
+ // see `resolveQuery`.
133
136
  return true;
134
137
  }
135
138
  /**
@@ -139,7 +142,89 @@ function indexedRecordMatchesQuery(entry, query) {
139
142
  * @public
140
143
  */
141
144
  function selectByQuery(entries, query) {
142
- return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);
145
+ return entries.filter((entry) => indexedRecordMatchesQuery(entry, query));
146
+ }
147
+ /**
148
+ * The shared select → order → page → materialize pipeline every non-semantic
149
+ * retriever runs, and the one place the body-vs-envelope ordering decision lives.
150
+ *
151
+ * @public
152
+ * @remarks
153
+ * **Without `query.filter`, ordering and paging happen over envelopes and only
154
+ * the page is materialized** — so `limit` genuinely bounds the number of records
155
+ * read, not merely the number returned. That is what makes `limit` a legitimate
156
+ * narrowing axis rather than a loophole, and it is conditional on the ordering
157
+ * key being an envelope field: both shipped comparators (`recencyCompare`,
158
+ * `rankCompare`) read `updated` / `seq` / `rank` and qualify. **An ordering that
159
+ * keyed on a body field could not use this path** and would have to sort after
160
+ * materialization, at which point `limit` bounds the result and not the read.
161
+ *
162
+ * **With `query.filter`, every envelope-survivor must be materialized first**,
163
+ * because the predicate takes a whole record. Semantics are preserved exactly;
164
+ * the cost is not. Pair `filter` with an envelope axis when the read cost
165
+ * matters.
166
+ */
167
+ function resolveQuery(entries, query, resolver) {
168
+ return materializePage(selectByQuery(entries, query), query, resolver, (candidates) => [...candidates].sort(orderingCompare(query.orderBy)));
169
+ }
170
+ /**
171
+ * Materialize a selected, ordered page — **applying `query.filter` and paging in
172
+ * the right order**, which is the whole reason this is shared rather than
173
+ * open-coded per retriever.
174
+ *
175
+ * @remarks
176
+ * Two paths, and the choice is forced by where the predicate can run:
177
+ *
178
+ * - **No `filter`** — order and page over *envelopes*, then read only the page.
179
+ * `limit` bounds the READ, not just the result.
180
+ * - **With `filter`** — the predicate takes a whole record, so every
181
+ * envelope-survivor must be read first, then filtered, then paged. Paging
182
+ * before filtering would return fewer than `limit` rows for no reason a caller
183
+ * could see.
184
+ *
185
+ * **Every retriever must route through this.** `indexedRecordMatchesQuery`
186
+ * structurally *cannot* apply `filter` — it is handed an envelope — so a
187
+ * retriever that pre-filters with it and then materializes on its own silently
188
+ * ignores the predicate. That regression shipped once, in the stream that moved
189
+ * `filter` out of the pre-filter; this function exists so it cannot recur.
190
+ *
191
+ * @param selected - Entries surviving the envelope pre-filter.
192
+ * @param query - The query whose `filter` / `orderBy` / `limit` / `offset` apply.
193
+ * @param resolver - Body resolver.
194
+ * @param order - Applied to whichever collection is paged; identity is legal for
195
+ * a retriever whose ordering is intrinsic (semantic score, traversal order).
196
+ * @public
197
+ */
198
+ function materializePage(selected, query, resolver, order) {
199
+ const ordered = order === undefined ? selected : order(selected);
200
+ if (query.filter === undefined) {
201
+ return materializeEntries(limitEntries(ordered, query.limit, query.offset), resolver);
202
+ }
203
+ const predicate = query.filter;
204
+ return materializeEntries(ordered, resolver).onSuccess((records) => (0, ts_utils_1.succeed)(limitRecords(records.filter(predicate), query.limit, query.offset)));
205
+ }
206
+ /**
207
+ * Materialize entries through the resolver, dropping any that have vanished
208
+ * since selection — a concurrent delete between selecting an envelope and
209
+ * reading its body is a legitimate race and yields a shorter list, not an error.
210
+ * A read that FAILS is a real fault and propagates.
211
+ * @public
212
+ */
213
+ function materializeEntries(entries, resolver) {
214
+ return (0, ts_utils_1.mapResults)(entries.map((e) => resolver.resolveRecord(e.scope, e.envelope.id))).onSuccess((records) => (0, ts_utils_1.succeed)(records.filter((r) => r !== undefined)));
215
+ }
216
+ /**
217
+ * The {@link limitRecords} window, applied to entries — used on the
218
+ * no-body-filter path so paging happens before anything is read.
219
+ * @public
220
+ */
221
+ function limitEntries(entries, limit, offset) {
222
+ const skip = offset !== undefined && offset > 0 ? offset : 0;
223
+ const windowed = skip > 0 ? entries.slice(skip) : entries;
224
+ if (limit === undefined) {
225
+ return windowed;
226
+ }
227
+ return limit > 0 ? windowed.slice(0, limit) : [];
143
228
  }
144
229
  /**
145
230
  * Apply the `{ offset, limit }` page window to an ordered record set. Applied
@@ -1 +1 @@
1
- {"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.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 { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';\nimport { IIndexedMemoryRecord } from '../index';\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /**\n * Restrict to records whose {@link IProvenance.source | provenance.source} is\n * exactly this value — the \"show me everything this source produced\" axis, for\n * review, attribution, and retraction after a bad ingest.\n *\n * @remarks\n * Exact match on `source` only, deliberately: not a subset match over the whole\n * {@link IProvenance} block, not a presence check, and not a match on `by` /\n * `model` / the consumer-owned extension keys. `source` is the only field of\n * that block the library assigns meaning to, and every record carries one\n * (`envelope.provenance` is required), so this axis is total — it partitions\n * the vault rather than filtering out records that merely omit a field.\n *\n * Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every\n * retriever honours it and combining axes composes as AND.\n */\n readonly provenanceSource?: ProvenanceSource;\n /**\n * Restrict to records of this kind — the single-kind shorthand for\n * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND\n * (the record's kind must satisfy both), so `kind` must itself be a member of\n * `kinds` for anything to match.\n */\n readonly kind?: Kind;\n /**\n * Restrict to records in ANY of these kinds — the general (multi-kind) form of\n * {@link IMemoryQuery.kind | kind}. Absent → no kind-set constraint (today's\n * behavior). An explicit empty array `[]` matches NOTHING (mirroring the\n * non-positive-`limit` \"explicit empty\" convention), never \"match all\".\n */\n readonly kinds?: ReadonlyArray<Kind>;\n /** Restrict to records linked FROM this scope-qualified seed (outbound). */\n readonly linkedFrom?: IEdgeTarget;\n /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */\n readonly linkedTo?: IEdgeTarget;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /**\n * Ordering for the result set. `'recency'` (the default when absent — today's\n * exact behavior) orders most-recently-updated first; `'rank'` orders by the\n * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent\n * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`\n * this yields a bounded top-M rank-ordered page with no full-vault scan.\n *\n * @remarks\n * `orderBy` governs the ordered non-semantic retrievers (recency / tag /\n * structured-filter / link-traversal) and the {@link HybridRetriever}'s\n * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it\n * preserves its native vector-similarity order regardless of `orderBy` —\n * re-sorting semantic hits by `rank` would discard the similarity ranking that\n * is the whole point of that path; a consumer that wants rank ordering uses a\n * non-semantic query.\n */\n readonly orderBy?: 'recency' | 'rank';\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /**\n * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a\n * stable page window over the ordered result set. Default 0. A non-positive or\n * absent offset is today's behavior (no skip); an offset past the end yields an\n * empty page, never a throw.\n */\n readonly offset?: number;\n /** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with\n * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST\n * (after every ranked record), then by recency among themselves. Mirrors the\n * index's rank-view ordering.\n * @public\n */\nexport function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n return recencyCompare(a, b);\n}\n\n/**\n * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}\n * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the\n * default, byte-identical to the pre-`orderBy` behavior).\n * @public\n */\nexport function orderingCompare(\n orderBy?: IMemoryQuery['orderBy']\n): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number {\n return orderBy === 'rank' ? rankCompare : recencyCompare;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag /\n * provenance-source / predicate pre-filter (the axes shared by every v1\n * retriever). The `semantic` / `asOf` / link axes are NOT applied here — those\n * are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {\n return false;\n }\n if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {\n return false;\n }\n if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (\n query.provenanceSource !== undefined &&\n entry.record.envelope.provenance.source !== query.provenanceSource\n ) {\n return false;\n }\n if (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / provenance-source / predicate pre-filter\n * to a set of indexed entries, returning the surviving records (unordered,\n * unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryRecord>,\n query: IMemoryQuery\n): IMemoryRecord<unknown>[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);\n}\n\n/**\n * Apply the `{ offset, limit }` page window to an ordered record set. Applied\n * last, after ordering, so it always takes a stable window of the ordered\n * result. `offset` is applied first (records to skip), then `limit` (top-N of\n * the remainder).\n *\n * @remarks\n * Both bounds are public query input and are guarded against non-positive\n * values slipping into `slice`:\n * - `offset` absent or non-positive → no skip (today's behavior). An offset past\n * the end yields an empty page rather than a throw.\n * - `limit` absent → no truncation; a non-positive `limit` means \"no records\"\n * and returns an empty array.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;\n if (limit === undefined) {\n return windowed;\n }\n if (limit <= 0) {\n return [];\n }\n return windowed.length > limit ? windowed.slice(0, limit) : windowed;\n}\n"]}
1
+ {"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport {\n IEdgeTarget,\n IEnvelopeCarrier,\n IMemoryRecord,\n IMemoryRecordResolver,\n Kind,\n MemoryScopeKey,\n ProvenanceSource,\n Tag\n} from '../types';\nimport { IIndexedMemoryEntry, IMemoryIndex } from '../index';\n\n/**\n * What every index-backed retriever needs: the index it selects over, and the\n * resolver it materializes survivors through.\n *\n * @remarks\n * The resolver is the whole of the partial-read migration for a caller that\n * constructs retrievers directly — the index projection itself is invisible to\n * anyone who only consumes `retrieve()`. `FileTreeMemoryStore` implements\n * {@link IMemoryRecordResolver}, so wiring is `{ index, resolver: store }`.\n * @public\n */\nexport interface IRetrieverCreateParams {\n /** The derived index to select over. */\n readonly index: IMemoryIndex;\n /** Materializes the selected entries' bodies. */\n readonly resolver: IMemoryRecordResolver;\n}\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /**\n * Restrict to records whose {@link IProvenance.source | provenance.source} is\n * exactly this value — the \"show me everything this source produced\" axis, for\n * review, attribution, and retraction after a bad ingest.\n *\n * @remarks\n * Exact match on `source` only, deliberately: not a subset match over the whole\n * {@link IProvenance} block, not a presence check, and not a match on `by` /\n * `model` / the consumer-owned extension keys. `source` is the only field of\n * that block the library assigns meaning to, and every record carries one\n * (`envelope.provenance` is required), so this axis is total — it partitions\n * the vault rather than filtering out records that merely omit a field.\n *\n * Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every\n * retriever honours it and combining axes composes as AND.\n */\n readonly provenanceSource?: ProvenanceSource;\n /**\n * Restrict to records of this kind — the single-kind shorthand for\n * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND\n * (the record's kind must satisfy both), so `kind` must itself be a member of\n * `kinds` for anything to match.\n */\n readonly kind?: Kind;\n /**\n * Restrict to records in ANY of these kinds — the general (multi-kind) form of\n * {@link IMemoryQuery.kind | kind}. Absent → no kind-set constraint (today's\n * behavior). An explicit empty array `[]` matches NOTHING (mirroring the\n * non-positive-`limit` \"explicit empty\" convention), never \"match all\".\n */\n readonly kinds?: ReadonlyArray<Kind>;\n /** Restrict to records linked FROM this scope-qualified seed (outbound). */\n readonly linkedFrom?: IEdgeTarget;\n /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */\n readonly linkedTo?: IEdgeTarget;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /**\n * Ordering for the result set. `'recency'` (the default when absent — today's\n * exact behavior) orders most-recently-updated first; `'rank'` orders by the\n * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent\n * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`\n * this yields a bounded top-M rank-ordered page with no full-vault scan.\n *\n * @remarks\n * `orderBy` governs the ordered non-semantic retrievers (recency / tag /\n * structured-filter / link-traversal) and the {@link HybridRetriever}'s\n * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it\n * preserves its native vector-similarity order regardless of `orderBy` —\n * re-sorting semantic hits by `rank` would discard the similarity ranking that\n * is the whole point of that path; a consumer that wants rank ordering uses a\n * non-semantic query.\n */\n readonly orderBy?: 'recency' | 'rank';\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /**\n * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a\n * stable page window over the ordered result set. Default 0. A non-positive or\n * absent offset is today's behavior (no skip); an offset past the end yields an\n * empty page, never a throw.\n */\n readonly offset?: number;\n /** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with\n * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST\n * (after every ranked record), then by recency among themselves. Mirrors the\n * index's rank-view ordering.\n * @public\n */\nexport function rankCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n return recencyCompare(a, b);\n}\n\n/**\n * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}\n * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the\n * default, byte-identical to the pre-`orderBy` behavior).\n * @public\n */\nexport function orderingCompare(\n orderBy?: IMemoryQuery['orderBy']\n): (a: IEnvelopeCarrier, b: IEnvelopeCarrier) => number {\n return orderBy === 'rank' ? rankCompare : recencyCompare;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag /\n * provenance-source / predicate pre-filter (the axes shared by every v1\n * retriever). The `semantic` / `asOf` / link axes are NOT applied here — those\n * are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryEntry, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.envelope.kind !== query.kind) {\n return false;\n }\n if (query.kinds !== undefined && !query.kinds.includes(entry.envelope.kind)) {\n return false;\n }\n if (query.tag !== undefined && !entry.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (query.provenanceSource !== undefined && entry.envelope.provenance.source !== query.provenanceSource) {\n return false;\n }\n // `query.filter` is deliberately NOT applied here — it is a predicate over the\n // whole record, and the index holds envelopes. It runs after materialization;\n // see `resolveQuery`.\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / provenance-source / predicate pre-filter\n * to a set of indexed entries, returning the surviving records (unordered,\n * unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryEntry>,\n query: IMemoryQuery\n): IIndexedMemoryEntry[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query));\n}\n\n/**\n * The shared select → order → page → materialize pipeline every non-semantic\n * retriever runs, and the one place the body-vs-envelope ordering decision lives.\n *\n * @public\n * @remarks\n * **Without `query.filter`, ordering and paging happen over envelopes and only\n * the page is materialized** — so `limit` genuinely bounds the number of records\n * read, not merely the number returned. That is what makes `limit` a legitimate\n * narrowing axis rather than a loophole, and it is conditional on the ordering\n * key being an envelope field: both shipped comparators (`recencyCompare`,\n * `rankCompare`) read `updated` / `seq` / `rank` and qualify. **An ordering that\n * keyed on a body field could not use this path** and would have to sort after\n * materialization, at which point `limit` bounds the result and not the read.\n *\n * **With `query.filter`, every envelope-survivor must be materialized first**,\n * because the predicate takes a whole record. Semantics are preserved exactly;\n * the cost is not. Pair `filter` with an envelope axis when the read cost\n * matters.\n */\nexport function resolveQuery(\n entries: ReadonlyArray<IIndexedMemoryEntry>,\n query: IMemoryQuery,\n resolver: IMemoryRecordResolver\n): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n return materializePage(selectByQuery(entries, query), query, resolver, (candidates) =>\n [...candidates].sort(orderingCompare(query.orderBy))\n );\n}\n\n/**\n * Materialize a selected, ordered page — **applying `query.filter` and paging in\n * the right order**, which is the whole reason this is shared rather than\n * open-coded per retriever.\n *\n * @remarks\n * Two paths, and the choice is forced by where the predicate can run:\n *\n * - **No `filter`** — order and page over *envelopes*, then read only the page.\n * `limit` bounds the READ, not just the result.\n * - **With `filter`** — the predicate takes a whole record, so every\n * envelope-survivor must be read first, then filtered, then paged. Paging\n * before filtering would return fewer than `limit` rows for no reason a caller\n * could see.\n *\n * **Every retriever must route through this.** `indexedRecordMatchesQuery`\n * structurally *cannot* apply `filter` — it is handed an envelope — so a\n * retriever that pre-filters with it and then materializes on its own silently\n * ignores the predicate. That regression shipped once, in the stream that moved\n * `filter` out of the pre-filter; this function exists so it cannot recur.\n *\n * @param selected - Entries surviving the envelope pre-filter.\n * @param query - The query whose `filter` / `orderBy` / `limit` / `offset` apply.\n * @param resolver - Body resolver.\n * @param order - Applied to whichever collection is paged; identity is legal for\n * a retriever whose ordering is intrinsic (semantic score, traversal order).\n * @public\n */\nexport function materializePage<T extends IIndexedMemoryEntry>(\n selected: ReadonlyArray<T>,\n query: IMemoryQuery,\n resolver: IMemoryRecordResolver,\n order?: (candidates: ReadonlyArray<T>) => ReadonlyArray<T>\n): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const ordered: ReadonlyArray<T> = order === undefined ? selected : order(selected);\n if (query.filter === undefined) {\n return materializeEntries(limitEntries(ordered, query.limit, query.offset), resolver);\n }\n const predicate: (record: IMemoryRecord<unknown>) => boolean = query.filter;\n return materializeEntries(ordered, resolver).onSuccess((records) =>\n succeed(limitRecords(records.filter(predicate), query.limit, query.offset))\n );\n}\n\n/**\n * Materialize entries through the resolver, dropping any that have vanished\n * since selection — a concurrent delete between selecting an envelope and\n * reading its body is a legitimate race and yields a shorter list, not an error.\n * A read that FAILS is a real fault and propagates.\n * @public\n */\nexport function materializeEntries(\n entries: ReadonlyArray<IIndexedMemoryEntry>,\n resolver: IMemoryRecordResolver\n): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n return mapResults(entries.map((e) => resolver.resolveRecord(e.scope, e.envelope.id))).onSuccess((records) =>\n succeed(records.filter((r): r is IMemoryRecord<unknown> => r !== undefined))\n );\n}\n\n/**\n * The {@link limitRecords} window, applied to entries — used on the\n * no-body-filter path so paging happens before anything is read.\n * @public\n */\nexport function limitEntries(\n entries: ReadonlyArray<IIndexedMemoryEntry>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IIndexedMemoryEntry> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IIndexedMemoryEntry> = skip > 0 ? entries.slice(skip) : entries;\n if (limit === undefined) {\n return windowed;\n }\n return limit > 0 ? windowed.slice(0, limit) : [];\n}\n\n/**\n * Apply the `{ offset, limit }` page window to an ordered record set. Applied\n * last, after ordering, so it always takes a stable window of the ordered\n * result. `offset` is applied first (records to skip), then `limit` (top-N of\n * the remainder).\n *\n * @remarks\n * Both bounds are public query input and are guarded against non-positive\n * values slipping into `slice`:\n * - `offset` absent or non-positive → no skip (today's behavior). An offset past\n * the end yields an empty page rather than a throw.\n * - `limit` absent → no truncation; a non-positive `limit` means \"no records\"\n * and returns an empty array.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;\n if (limit === undefined) {\n return windowed;\n }\n if (limit <= 0) {\n return [];\n }\n return windowed.length > limit ? windowed.slice(0, limit) : windowed;\n}\n"]}
@@ -1,8 +1,7 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
2
  import { IMemoryRecord } from '../types';
3
- import { IMemoryIndex } from '../index';
4
3
  import { IVectorIndex } from '../vector';
5
- import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './retriever';
4
+ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities, IRetrieverCreateParams } from './retriever';
6
5
  /**
7
6
  * Embeds a query string into a vector for {@link IVectorIndex.query}. Async and
8
7
  * `Result`-returning, since a real embedder does a network call.
@@ -26,9 +25,7 @@ export interface ISemanticBackend {
26
25
  * Construction options for {@link SemanticRetriever.create}.
27
26
  * @public
28
27
  */
29
- export interface ISemanticRetrieverCreateParams {
30
- /** The record index, used to resolve vector hits back to full records. */
31
- readonly index: IMemoryIndex;
28
+ export interface ISemanticRetrieverCreateParams extends IRetrieverCreateParams {
32
29
  /**
33
30
  * The semantic backend. When absent, the retriever reports
34
31
  * `supportsSemanticRecall: false` and a `query.semantic` request degrades
@@ -50,6 +47,7 @@ export interface ISemanticRetrieverCreateParams {
50
47
  */
51
48
  export declare class SemanticRetriever implements IMemoryRetriever {
52
49
  private readonly _index;
50
+ private readonly _resolver;
53
51
  private readonly _backend;
54
52
  private constructor();
55
53
  /** {@inheritDoc IMemoryRetriever.capabilities} */
@@ -1 +1 @@
1
- {"version":3,"file":"semanticRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AACxD,OAAO,EAAwB,YAAY,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAmB,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAK7B,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC7C,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+B;IAExD,OAAO;IAKP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAMtD;IAED,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE,8BAA8B,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAIvF,8CAA8C;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IA+ClG;;;;;OAKG;mBACkB,YAAY;CAOlC"}
1
+ {"version":3,"file":"semanticRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,aAAa,EAAyB,MAAM,UAAU,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAmB,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAC5B,sBAAsB,EAKvB,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA+B,SAAQ,sBAAsB;IAC5E;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+B;IAExD,OAAO;IAMP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAMtD;IAED,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE,8BAA8B,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAIvF,8CAA8C;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IA6ClG;;;;;OAKG;mBACkB,YAAY;CAOlC"}
@@ -6,7 +6,6 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.SemanticRetriever = void 0;
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
- const types_1 = require("../types");
10
9
  const retriever_1 = require("./retriever");
11
10
  /**
12
11
  * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it
@@ -21,9 +20,10 @@ const retriever_1 = require("./retriever");
21
20
  * @public
22
21
  */
23
22
  class SemanticRetriever {
24
- constructor(index, backend) {
25
- this._index = index;
26
- this._backend = backend;
23
+ constructor(params) {
24
+ this._index = params.index;
25
+ this._resolver = params.resolver;
26
+ this._backend = params.backend;
27
27
  }
28
28
  /** {@inheritDoc IMemoryRetriever.capabilities} */
29
29
  get capabilities() {
@@ -35,7 +35,7 @@ class SemanticRetriever {
35
35
  }
36
36
  /** Family-convention factory. */
37
37
  static create(params) {
38
- return (0, ts_utils_1.succeed)(new SemanticRetriever(params.index, params.backend));
38
+ return (0, ts_utils_1.succeed)(new SemanticRetriever(params));
39
39
  }
40
40
  /** {@inheritDoc IMemoryRetriever.retrieve} */
41
41
  async retrieve(query) {
@@ -62,20 +62,20 @@ class SemanticRetriever {
62
62
  if (hits.isFailure()) {
63
63
  return (0, ts_utils_1.fail)(hits.message);
64
64
  }
65
- // Key by the canonical scope-qualified target so a hit re-resolves to the
66
- // exact record it scored against a bare id would alias two records that
67
- // share a filename stem across scopes.
68
- const byKey = new Map(this._index
69
- .entries()
70
- .map((entry) => [(0, types_1.edgeTargetKey)({ scope: entry.scope, id: entry.record.envelope.id }), entry]));
71
- const records = [];
65
+ // Resolve each hit by its canonical scope-qualified target a bare id would
66
+ // alias two records that share a filename stem across scopes. This used to
67
+ // build a Map over the ENTIRE index to look up at most `topK` of them; the
68
+ // index's own `get` makes it O(hits).
69
+ const selected = [];
72
70
  for (const hit of hits.value) {
73
- const entry = byKey.get((0, types_1.edgeTargetKey)(hit.target));
71
+ const entry = this._index.get(hit.target);
74
72
  if (entry !== undefined && (0, retriever_1.indexedRecordMatchesQuery)(entry, query)) {
75
- records.push(entry.record);
73
+ selected.push(entry);
76
74
  }
77
75
  }
78
- return (0, ts_utils_1.succeed)((0, retriever_1.limitRecords)(records, query.limit, query.offset));
76
+ // Hit order IS the ranking, so the page is taken before materializing and
77
+ // only the returned records are read.
78
+ return (0, retriever_1.materializePage)(selected, query, this._resolver);
79
79
  }
80
80
  /**
81
81
  * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
@@ -1 +1 @@
1
- {"version":3,"file":"semanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.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, edgeTargetKey } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport { IVectorIndex, IVectorQueryHit } from '../vector';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n SEMANTIC_UNWIRED_MESSAGE,\n indexedRecordMatchesQuery,\n limitRecords,\n temporalUnwiredMessage\n} from './retriever';\n\n/**\n * Embeds a query string into a vector for {@link IVectorIndex.query}. Async and\n * `Result`-returning, since a real embedder does a network call.\n * @public\n */\nexport type QueryEmbedder = (text: string) => Promise<Result<Float32Array>>;\n\n/**\n * The semantic backend wired into a {@link SemanticRetriever}: the vector index\n * to query and the embedder that turns the query text into a vector. Both are\n * required together — a vector index is useless without a way to embed the\n * query, so {@link SemanticRetriever.create} treats them as one unit.\n * @public\n */\nexport interface ISemanticBackend {\n /** The vector index to query. */\n readonly vectorIndex: IVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * Construction options for {@link SemanticRetriever.create}.\n * @public\n */\nexport interface ISemanticRetrieverCreateParams {\n /** The record index, used to resolve vector hits back to full records. */\n readonly index: IMemoryIndex;\n /**\n * The semantic backend. When absent, the retriever reports\n * `supportsSemanticRecall: false` and a `query.semantic` request degrades\n * loudly ({@link SEMANTIC_UNWIRED_MESSAGE}) rather than returning empty.\n */\n readonly backend?: ISemanticBackend;\n}\n\n/**\n * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it\n * embeds `query.semantic`, queries the vector index, and resolves the hits back\n * to records (preserving vector score order). When no backend is wired,\n * `supportsSemanticRecall` is `false` and any `query.semantic` request degrades\n * loudly — it NEVER returns a silent empty.\n *\n * @remarks\n * A consumer-supplied backend that rejects (throws) is normalized into a\n * `Failure` — `retrieve` always honors its `Promise<Result<...>>` contract.\n * @public\n */\nexport class SemanticRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n private readonly _backend: ISemanticBackend | undefined;\n\n private constructor(index: IMemoryIndex, backend: ISemanticBackend | undefined) {\n this._index = index;\n this._backend = backend;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return {\n supportsSemanticRecall: this._backend !== undefined,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n };\n }\n\n /** Family-convention factory. */\n public static create(params: ISemanticRetrieverCreateParams): Result<SemanticRetriever> {\n return succeed(new SemanticRetriever(params.index, params.backend));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n if (query.asOf !== undefined) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n if (query.semantic === undefined) {\n // The semantic term is this retriever's axis; without it there is nothing\n // to recall (a no-op contribution to a HybridRetriever, not a failure).\n return succeed([]);\n }\n if (this._backend === undefined) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: ISemanticBackend = this._backend;\n // The backend hooks are consumer-supplied; a rejecting (throwing) impl must\n // still surface as a `Failure`, never escape `retrieve` as a rejected\n // promise. `_callBackend` normalizes both a returned `fail` and a rejection.\n const embedded: Result<Float32Array> = await SemanticRetriever._callBackend('query embedding', () =>\n backend.embedQuery(query.semantic as string)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n const hits: Result<ReadonlyArray<IVectorQueryHit>> = await SemanticRetriever._callBackend(\n 'vector query',\n () => backend.vectorIndex.query(embedded.value, query.topK ?? 10)\n );\n if (hits.isFailure()) {\n return fail(hits.message);\n }\n // Key by the canonical scope-qualified target so a hit re-resolves to the\n // exact record it scored against — a bare id would alias two records that\n // share a filename stem across scopes.\n const byKey: Map<string, IIndexedMemoryRecord> = new Map(\n this._index\n .entries()\n .map((entry) => [edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry])\n );\n const records: IMemoryRecord<unknown>[] = [];\n for (const hit of hits.value) {\n const entry: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(hit.target));\n if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {\n records.push(entry.record);\n }\n }\n return succeed(limitRecords(records, query.limit, query.offset));\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `semantic recall: <label> failed`\n * `Failure`. Keeps `retrieve` within the `Promise<Result<...>>` contract even\n * when the injected `embedQuery` / `vectorIndex` misbehaves.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `semantic recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`semantic recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"semanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.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, IMemoryRecordResolver } from '../types';\nimport { IIndexedMemoryEntry, IMemoryIndex } from '../index';\nimport { IVectorIndex, IVectorQueryHit } from '../vector';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n IRetrieverCreateParams,\n SEMANTIC_UNWIRED_MESSAGE,\n indexedRecordMatchesQuery,\n materializePage,\n temporalUnwiredMessage\n} from './retriever';\n\n/**\n * Embeds a query string into a vector for {@link IVectorIndex.query}. Async and\n * `Result`-returning, since a real embedder does a network call.\n * @public\n */\nexport type QueryEmbedder = (text: string) => Promise<Result<Float32Array>>;\n\n/**\n * The semantic backend wired into a {@link SemanticRetriever}: the vector index\n * to query and the embedder that turns the query text into a vector. Both are\n * required together — a vector index is useless without a way to embed the\n * query, so {@link SemanticRetriever.create} treats them as one unit.\n * @public\n */\nexport interface ISemanticBackend {\n /** The vector index to query. */\n readonly vectorIndex: IVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * Construction options for {@link SemanticRetriever.create}.\n * @public\n */\nexport interface ISemanticRetrieverCreateParams extends IRetrieverCreateParams {\n /**\n * The semantic backend. When absent, the retriever reports\n * `supportsSemanticRecall: false` and a `query.semantic` request degrades\n * loudly ({@link SEMANTIC_UNWIRED_MESSAGE}) rather than returning empty.\n */\n readonly backend?: ISemanticBackend;\n}\n\n/**\n * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it\n * embeds `query.semantic`, queries the vector index, and resolves the hits back\n * to records (preserving vector score order). When no backend is wired,\n * `supportsSemanticRecall` is `false` and any `query.semantic` request degrades\n * loudly — it NEVER returns a silent empty.\n *\n * @remarks\n * A consumer-supplied backend that rejects (throws) is normalized into a\n * `Failure` — `retrieve` always honors its `Promise<Result<...>>` contract.\n * @public\n */\nexport class SemanticRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n private readonly _resolver: IMemoryRecordResolver;\n private readonly _backend: ISemanticBackend | undefined;\n\n private constructor(params: ISemanticRetrieverCreateParams) {\n this._index = params.index;\n this._resolver = params.resolver;\n this._backend = params.backend;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return {\n supportsSemanticRecall: this._backend !== undefined,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n };\n }\n\n /** Family-convention factory. */\n public static create(params: ISemanticRetrieverCreateParams): Result<SemanticRetriever> {\n return succeed(new SemanticRetriever(params));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n if (query.asOf !== undefined) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n if (query.semantic === undefined) {\n // The semantic term is this retriever's axis; without it there is nothing\n // to recall (a no-op contribution to a HybridRetriever, not a failure).\n return succeed([]);\n }\n if (this._backend === undefined) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: ISemanticBackend = this._backend;\n // The backend hooks are consumer-supplied; a rejecting (throwing) impl must\n // still surface as a `Failure`, never escape `retrieve` as a rejected\n // promise. `_callBackend` normalizes both a returned `fail` and a rejection.\n const embedded: Result<Float32Array> = await SemanticRetriever._callBackend('query embedding', () =>\n backend.embedQuery(query.semantic as string)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n const hits: Result<ReadonlyArray<IVectorQueryHit>> = await SemanticRetriever._callBackend(\n 'vector query',\n () => backend.vectorIndex.query(embedded.value, query.topK ?? 10)\n );\n if (hits.isFailure()) {\n return fail(hits.message);\n }\n // Resolve each hit by its canonical scope-qualified target a bare id would\n // alias two records that share a filename stem across scopes. This used to\n // build a Map over the ENTIRE index to look up at most `topK` of them; the\n // index's own `get` makes it O(hits).\n const selected: IIndexedMemoryEntry[] = [];\n for (const hit of hits.value) {\n const entry: IIndexedMemoryEntry | undefined = this._index.get(hit.target);\n if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {\n selected.push(entry);\n }\n }\n // Hit order IS the ranking, so the page is taken before materializing and\n // only the returned records are read.\n return materializePage(selected, query, this._resolver);\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `semantic recall: <label> failed`\n * `Failure`. Keeps `retrieve` within the `Promise<Result<...>>` contract even\n * when the injected `embedQuery` / `vectorIndex` misbehaves.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `semantic recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`semantic recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
@@ -1,7 +1,6 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
2
  import { IMemoryRecord } from '../types';
3
- import { IMemoryIndex } from '../index';
4
- import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './retriever';
3
+ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities, IRetrieverCreateParams } from './retriever';
5
4
  /**
6
5
  * Returns records matching `query.filter` or `query.provenanceSource`, narrowed
7
6
  * by any scope / kind / tag / provenance-source pre-filter and recency-ordered.
@@ -33,11 +32,12 @@ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './
33
32
  */
34
33
  export declare class StructuredFilterRetriever implements IMemoryRetriever {
35
34
  private readonly _index;
35
+ private readonly _resolver;
36
36
  private constructor();
37
37
  /** {@inheritDoc IMemoryRetriever.capabilities} */
38
38
  get capabilities(): IMemoryRetrieverCapabilities;
39
39
  /** Family-convention factory. */
40
- static create(index: IMemoryIndex): Result<StructuredFilterRetriever>;
40
+ static create(params: IRetrieverCreateParams): Result<StructuredFilterRetriever>;
41
41
  /** {@inheritDoc IMemoryRetriever.retrieve} */
42
42
  retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
43
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"structuredFilterRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,yBAA0B,YAAW,gBAAgB;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO;IAIP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,iCAAiC;WACnB,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,yBAAyB,CAAC;IAI5E,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CAa7F"}
1
+ {"version":3,"file":"structuredFilterRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAyB,MAAM,UAAU,CAAC;AAEhE,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAC5B,sBAAsB,EAIvB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,yBAA0B,YAAW,gBAAgB;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,OAAO;IAKP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAAC,yBAAyB,CAAC;IAIvF,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CAU7F"}
@@ -37,16 +37,17 @@ const retriever_1 = require("./retriever");
37
37
  * @public
38
38
  */
39
39
  class StructuredFilterRetriever {
40
- constructor(index) {
41
- this._index = index;
40
+ constructor(params) {
41
+ this._index = params.index;
42
+ this._resolver = params.resolver;
42
43
  }
43
44
  /** {@inheritDoc IMemoryRetriever.capabilities} */
44
45
  get capabilities() {
45
46
  return retriever_1.NON_SEMANTIC_CAPABILITIES;
46
47
  }
47
48
  /** Family-convention factory. */
48
- static create(index) {
49
- return (0, ts_utils_1.succeed)(new StructuredFilterRetriever(index));
49
+ static create(params) {
50
+ return (0, ts_utils_1.succeed)(new StructuredFilterRetriever(params));
50
51
  }
51
52
  /** {@inheritDoc IMemoryRetriever.retrieve} */
52
53
  retrieve(query) {
@@ -54,8 +55,7 @@ class StructuredFilterRetriever {
54
55
  if (query.filter === undefined && query.provenanceSource === undefined) {
55
56
  return (0, ts_utils_1.succeed)([]);
56
57
  }
57
- const ordered = (0, retriever_1.selectByQuery)(this._index.entries(), query).sort((0, retriever_1.orderingCompare)(query.orderBy));
58
- return (0, ts_utils_1.succeed)((0, retriever_1.limitRecords)(ordered, query.limit, query.offset));
58
+ return (0, retriever_1.resolveQuery)(this._index.entries(), query, this._resolver);
59
59
  }));
60
60
  }
61
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAgD;AAGhD,2CASqB;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAa,yBAAyB;IAGpC,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;gBACvE,OAAO,IAAA,kBAAO,EAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,IAAA,yBAAa,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,IAAA,2BAAe,EAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,IAAA,kBAAO,EAAC,IAAA,wBAAY,EAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA/BD,8DA+BC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching `query.filter` or `query.provenanceSource`, narrowed\n * by any scope / kind / tag / provenance-source pre-filter and recency-ordered.\n * Those two are this retriever's axes: a query carrying neither is not its\n * concern and yields an empty success (so it contributes nothing to a\n * {@link HybridRetriever}, rather than failing).\n *\n * Note `provenanceSource` appears on both sides of that sentence, and the\n * duplication is real rather than sloppy: it is one of this retriever's two\n * *dispatch* axes (it decides whether the query is this retriever's concern at\n * all) **and** a member of the shared *pre-filter* (it narrows the result set,\n * as it does for every other retriever). `filter` is likewise both.\n *\n * @remarks\n * `provenanceSource` is *applied* by the shared pre-filter, so every retriever\n * narrows by it. What this retriever adds is *answering* a query whose only axis\n * is `provenanceSource` — the \"show me everything this source produced\" request,\n * which would otherwise fall through the `filter`-absent guard and come back\n * empty.\n *\n * Consequently, inside a {@link HybridRetriever} composed with the universal\n * {@link RecencyRetriever}, a `provenanceSource`-only query is answered by both\n * children and every matching record scores twice under a score-union merge.\n * That is the established behavior for a dedicated-axis retriever composed with\n * the universal one — {@link TagRetriever} double-scores a `tag`-only query the\n * same way — and is intentional here, not an artifact of grafting a second axis\n * onto a retriever whose original concern was arbitrary predicates.\n * @public\n */\nexport class StructuredFilterRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<StructuredFilterRetriever> {\n return succeed(new StructuredFilterRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.filter === undefined && query.provenanceSource === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
1
+ {"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAgD;AAGhD,2CAQqB;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAa,yBAAyB;IAIpC,YAAoB,MAA8B;QAChD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC;IACnC,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,MAA8B;QACjD,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;gBACvE,OAAO,IAAA,kBAAO,EAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,OAAO,IAAA,wBAAY,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACpE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA9BD,8DA8BC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, IMemoryRecordResolver } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n IRetrieverCreateParams,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n resolveQuery\n} from './retriever';\n\n/**\n * Returns records matching `query.filter` or `query.provenanceSource`, narrowed\n * by any scope / kind / tag / provenance-source pre-filter and recency-ordered.\n * Those two are this retriever's axes: a query carrying neither is not its\n * concern and yields an empty success (so it contributes nothing to a\n * {@link HybridRetriever}, rather than failing).\n *\n * Note `provenanceSource` appears on both sides of that sentence, and the\n * duplication is real rather than sloppy: it is one of this retriever's two\n * *dispatch* axes (it decides whether the query is this retriever's concern at\n * all) **and** a member of the shared *pre-filter* (it narrows the result set,\n * as it does for every other retriever). `filter` is likewise both.\n *\n * @remarks\n * `provenanceSource` is *applied* by the shared pre-filter, so every retriever\n * narrows by it. What this retriever adds is *answering* a query whose only axis\n * is `provenanceSource` — the \"show me everything this source produced\" request,\n * which would otherwise fall through the `filter`-absent guard and come back\n * empty.\n *\n * Consequently, inside a {@link HybridRetriever} composed with the universal\n * {@link RecencyRetriever}, a `provenanceSource`-only query is answered by both\n * children and every matching record scores twice under a score-union merge.\n * That is the established behavior for a dedicated-axis retriever composed with\n * the universal one — {@link TagRetriever} double-scores a `tag`-only query the\n * same way — and is intentional here, not an artifact of grafting a second axis\n * onto a retriever whose original concern was arbitrary predicates.\n * @public\n */\nexport class StructuredFilterRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n private readonly _resolver: IMemoryRecordResolver;\n\n private constructor(params: IRetrieverCreateParams) {\n this._index = params.index;\n this._resolver = params.resolver;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(params: IRetrieverCreateParams): Result<StructuredFilterRetriever> {\n return succeed(new StructuredFilterRetriever(params));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.filter === undefined && query.provenanceSource === undefined) {\n return succeed([]);\n }\n return resolveQuery(this._index.entries(), query, this._resolver);\n })\n );\n }\n}\n"]}