@fgv/ts-agent-memory 5.1.0-48 → 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.
- package/dist/packlets/index/memoryIndex.js +23 -16
- package/dist/packlets/index/memoryIndex.js.map +1 -1
- package/dist/packlets/ingest/orchestrator.js +13 -1
- package/dist/packlets/ingest/orchestrator.js.map +1 -1
- package/dist/packlets/retrieve/linkTraversalRetriever.js +13 -27
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +8 -8
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +98 -11
- package/dist/packlets/retrieve/retriever.js.map +1 -1
- package/dist/packlets/retrieve/semanticRetriever.js +16 -16
- package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/dist/packlets/retrieve/structuredFilterRetriever.js +34 -12
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +8 -8
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +24 -21
- package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/dist/packlets/store/coverage.js +6 -0
- package/dist/packlets/store/coverage.js.map +1 -0
- package/dist/packlets/store/fileTreeMemoryStore.js +303 -62
- package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/dist/packlets/store/index.js +4 -0
- package/dist/packlets/store/index.js.map +1 -1
- package/dist/packlets/store/listSelection.js +36 -0
- package/dist/packlets/store/listSelection.js.map +1 -0
- package/dist/packlets/store/memoryStore.js +6 -0
- package/dist/packlets/store/memoryStore.js.map +1 -0
- package/dist/packlets/store/reconcile.js +6 -0
- package/dist/packlets/store/reconcile.js.map +1 -0
- package/dist/packlets/store/storeCoverage.js +102 -0
- package/dist/packlets/store/storeCoverage.js.map +1 -0
- package/dist/packlets/store/storeReconcile.js +122 -0
- package/dist/packlets/store/storeReconcile.js.map +1 -0
- package/dist/packlets/store/vectorMaintenance.js +116 -8
- package/dist/packlets/store/vectorMaintenance.js.map +1 -1
- package/dist/packlets/store/vectorRecordSource.js +44 -0
- package/dist/packlets/store/vectorRecordSource.js.map +1 -0
- package/dist/packlets/tools/memoryTools.js +25 -2
- package/dist/packlets/tools/memoryTools.js.map +1 -1
- package/dist/packlets/types/envelope.js +25 -0
- package/dist/packlets/types/envelope.js.map +1 -1
- package/dist/packlets/types/index.js +1 -0
- package/dist/packlets/types/index.js.map +1 -1
- package/dist/packlets/types/recordResolver.js +6 -0
- package/dist/packlets/types/recordResolver.js.map +1 -0
- package/dist/packlets/types/temporal.js.map +1 -1
- package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
- package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +67 -12
- package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/dist/packlets/vector/rebuildHelpers.js +38 -0
- package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
- package/dist/packlets/vector/vectorIndex.js.map +1 -1
- package/dist/ts-agent-memory.d.ts +1140 -90
- package/lib/packlets/index/memoryIndex.d.ts +118 -27
- package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
- package/lib/packlets/index/memoryIndex.js +23 -16
- package/lib/packlets/index/memoryIndex.js.map +1 -1
- package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
- package/lib/packlets/ingest/orchestrator.js +13 -1
- package/lib/packlets/ingest/orchestrator.js.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +4 -11
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +12 -26
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.d.ts +4 -4
- package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +7 -7
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +113 -13
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +101 -10
- package/lib/packlets/retrieve/retriever.js.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
- package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/semanticRetriever.js +15 -15
- package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +29 -7
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +33 -11
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.d.ts +4 -4
- package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +7 -7
- package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
- package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
- package/lib/packlets/retrieve/temporalRetrievers.js +23 -20
- package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
- package/lib/packlets/store/coverage.d.ts +102 -0
- package/lib/packlets/store/coverage.d.ts.map +1 -0
- package/lib/packlets/store/coverage.js +7 -0
- package/lib/packlets/store/coverage.js.map +1 -0
- package/lib/packlets/store/fileTreeMemoryStore.d.ts +88 -132
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +302 -61
- package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
- package/lib/packlets/store/index.d.ts +4 -0
- package/lib/packlets/store/index.d.ts.map +1 -1
- package/lib/packlets/store/index.js +4 -0
- package/lib/packlets/store/index.js.map +1 -1
- package/lib/packlets/store/listSelection.d.ts +101 -0
- package/lib/packlets/store/listSelection.d.ts.map +1 -0
- package/lib/packlets/store/listSelection.js +40 -0
- package/lib/packlets/store/listSelection.js.map +1 -0
- package/lib/packlets/store/memoryStore.d.ts +237 -0
- package/lib/packlets/store/memoryStore.d.ts.map +1 -0
- package/lib/packlets/store/memoryStore.js +7 -0
- package/lib/packlets/store/memoryStore.js.map +1 -0
- package/lib/packlets/store/reconcile.d.ts +82 -0
- package/lib/packlets/store/reconcile.d.ts.map +1 -0
- package/lib/packlets/store/reconcile.js +7 -0
- package/lib/packlets/store/reconcile.js.map +1 -0
- package/lib/packlets/store/storeCoverage.d.ts +45 -0
- package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
- package/lib/packlets/store/storeCoverage.js +105 -0
- package/lib/packlets/store/storeCoverage.js.map +1 -0
- package/lib/packlets/store/storeReconcile.d.ts +41 -0
- package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
- package/lib/packlets/store/storeReconcile.js +125 -0
- package/lib/packlets/store/storeReconcile.js.map +1 -0
- package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
- package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
- package/lib/packlets/store/vectorMaintenance.js +117 -8
- package/lib/packlets/store/vectorMaintenance.js.map +1 -1
- package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
- package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
- package/lib/packlets/store/vectorRecordSource.js +47 -0
- package/lib/packlets/store/vectorRecordSource.js.map +1 -0
- package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
- package/lib/packlets/tools/memoryTools.js +25 -2
- package/lib/packlets/tools/memoryTools.js.map +1 -1
- package/lib/packlets/types/envelope.d.ts +33 -0
- package/lib/packlets/types/envelope.d.ts.map +1 -1
- package/lib/packlets/types/envelope.js +26 -0
- package/lib/packlets/types/envelope.js.map +1 -1
- package/lib/packlets/types/index.d.ts +1 -0
- package/lib/packlets/types/index.d.ts.map +1 -1
- package/lib/packlets/types/index.js +1 -0
- package/lib/packlets/types/index.js.map +1 -1
- package/lib/packlets/types/recordResolver.d.ts +39 -0
- package/lib/packlets/types/recordResolver.d.ts.map +1 -0
- package/lib/packlets/types/recordResolver.js +7 -0
- package/lib/packlets/types/recordResolver.js.map +1 -0
- package/lib/packlets/types/temporal.d.ts +26 -6
- package/lib/packlets/types/temporal.d.ts.map +1 -1
- package/lib/packlets/types/temporal.js.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
- package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
- package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +6 -3
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
- package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
- package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
- package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
- package/lib/packlets/vector/rebuildHelpers.js +42 -0
- package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
- package/lib/packlets/vector/vectorIndex.d.ts +270 -15
- package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
- package/lib/packlets/vector/vectorIndex.js.map +1 -1
- package/package.json +7 -7
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, Tag } from '../types';
|
|
3
|
-
import {
|
|
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
|
|
@@ -27,6 +44,23 @@ export interface IMemoryQuery {
|
|
|
27
44
|
readonly scope?: MemoryScopeKey;
|
|
28
45
|
/** Restrict to records carrying this tag (exact match). */
|
|
29
46
|
readonly tag?: Tag;
|
|
47
|
+
/**
|
|
48
|
+
* Restrict to records whose {@link IProvenance.source | provenance.source} is
|
|
49
|
+
* exactly this value — the "show me everything this source produced" axis, for
|
|
50
|
+
* review, attribution, and retraction after a bad ingest.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Exact match on `source` only, deliberately: not a subset match over the whole
|
|
54
|
+
* {@link IProvenance} block, not a presence check, and not a match on `by` /
|
|
55
|
+
* `model` / the consumer-owned extension keys. `source` is the only field of
|
|
56
|
+
* that block the library assigns meaning to, and every record carries one
|
|
57
|
+
* (`envelope.provenance` is required), so this axis is total — it partitions
|
|
58
|
+
* the vault rather than filtering out records that merely omit a field.
|
|
59
|
+
*
|
|
60
|
+
* Applied in the shared pre-filter alongside `scope` / `kind` / `tag`, so every
|
|
61
|
+
* retriever honours it and combining axes composes as AND.
|
|
62
|
+
*/
|
|
63
|
+
readonly provenanceSource?: ProvenanceSource;
|
|
30
64
|
/**
|
|
31
65
|
* Restrict to records of this kind — the single-kind shorthand for
|
|
32
66
|
* {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND
|
|
@@ -87,7 +121,7 @@ export interface IMemoryQuery {
|
|
|
87
121
|
* empty page, never a throw.
|
|
88
122
|
*/
|
|
89
123
|
readonly offset?: number;
|
|
90
|
-
/** Arbitrary predicate applied after the scope / kind / tag pre-filter. */
|
|
124
|
+
/** Arbitrary predicate applied after the scope / kind / tag / provenance-source pre-filter. */
|
|
91
125
|
readonly filter?: (record: IMemoryRecord<unknown>) => boolean;
|
|
92
126
|
}
|
|
93
127
|
/**
|
|
@@ -144,7 +178,7 @@ export declare function guardRetrieverCapabilities(query: IMemoryQuery, capabili
|
|
|
144
178
|
* equal-`updated` records sort deterministically. Mirrors the B1 index ordering.
|
|
145
179
|
* @public
|
|
146
180
|
*/
|
|
147
|
-
export declare function recencyCompare(a:
|
|
181
|
+
export declare function recencyCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
|
|
148
182
|
/**
|
|
149
183
|
* Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with
|
|
150
184
|
* {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST
|
|
@@ -152,27 +186,93 @@ export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryReco
|
|
|
152
186
|
* index's rank-view ordering.
|
|
153
187
|
* @public
|
|
154
188
|
*/
|
|
155
|
-
export declare function rankCompare(a:
|
|
189
|
+
export declare function rankCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
|
|
156
190
|
/**
|
|
157
191
|
* Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}
|
|
158
192
|
* axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the
|
|
159
193
|
* default, byte-identical to the pre-`orderBy` behavior).
|
|
160
194
|
* @public
|
|
161
195
|
*/
|
|
162
|
-
export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a:
|
|
196
|
+
export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IEnvelopeCarrier, b: IEnvelopeCarrier) => number;
|
|
197
|
+
/**
|
|
198
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
199
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
200
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
201
|
+
* are each retriever's own concern.
|
|
202
|
+
* @public
|
|
203
|
+
*/
|
|
204
|
+
export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryEntry, query: IMemoryQuery): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
207
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
208
|
+
* unlimited).
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
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>>>;
|
|
163
262
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
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.
|
|
167
267
|
* @public
|
|
168
268
|
*/
|
|
169
|
-
export declare function
|
|
269
|
+
export declare function materializeEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, resolver: IMemoryRecordResolver): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
|
|
170
270
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
271
|
+
* The {@link limitRecords} window, applied to entries — used on the
|
|
272
|
+
* no-body-filter path so paging happens before anything is read.
|
|
173
273
|
* @public
|
|
174
274
|
*/
|
|
175
|
-
export declare function
|
|
275
|
+
export declare function limitEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, limit?: number, offset?: number): ReadonlyArray<IIndexedMemoryEntry>;
|
|
176
276
|
/**
|
|
177
277
|
* Apply the `{ offset, limit }` page window to an ordered record set. Applied
|
|
178
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":"AAKA,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAA6B,MAAM,eAAe,CAAC;AAClE,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,IAAI,EACJ,cAAc,EACd,gBAAgB,EAChB,GAAG,EACJ,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;CAC1C;AAED;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IACzC,4EAA4E;IAC5E,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IACnB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC;IAClC,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC;CAC/D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,4BAA4B,CAAC;IACpD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;CACvF;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAmE,CAAC;AAE3G;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,EAAE,MACgB,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,4BAIvC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAI1D;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,4BAA4B,GACzC,MAAM,CAAC,IAAI,CAAC,CAad;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAG/E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAa5E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,KAAK,MAAM,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAoBlG;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC3C,KAAK,EAAE,YAAY,GAClB,mBAAmB,EAAE,CAEvB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC3C,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,qBAAqB,GAC9B,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAI/C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,mBAAmB,EAC3D,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAC1B,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,qBAAqB,EAC/B,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,GACzD,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAS/C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC3C,QAAQ,EAAE,qBAAqB,GAC9B,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAC3C,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,aAAa,CAAC,mBAAmB,CAAC,CAOpC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAC9C,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAUvC"}
|
|
@@ -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
|
/**
|
|
@@ -104,36 +108,123 @@ function orderingCompare(orderBy) {
|
|
|
104
108
|
return orderBy === 'rank' ? rankCompare : recencyCompare;
|
|
105
109
|
}
|
|
106
110
|
/**
|
|
107
|
-
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
108
|
-
* pre-filter (the axes shared by every v1
|
|
109
|
-
* link axes are NOT applied here — those
|
|
111
|
+
* Whether an indexed entry satisfies a query's scope / kind / tag /
|
|
112
|
+
* provenance-source / predicate pre-filter (the axes shared by every v1
|
|
113
|
+
* retriever). The `semantic` / `asOf` / link axes are NOT applied here — those
|
|
114
|
+
* are each retriever's own concern.
|
|
110
115
|
* @public
|
|
111
116
|
*/
|
|
112
117
|
function indexedRecordMatchesQuery(entry, query) {
|
|
113
118
|
if (query.scope !== undefined && entry.scope !== query.scope) {
|
|
114
119
|
return false;
|
|
115
120
|
}
|
|
116
|
-
if (query.kind !== undefined && entry.
|
|
121
|
+
if (query.kind !== undefined && entry.envelope.kind !== query.kind) {
|
|
117
122
|
return false;
|
|
118
123
|
}
|
|
119
|
-
if (query.kinds !== undefined && !query.kinds.includes(entry.
|
|
124
|
+
if (query.kinds !== undefined && !query.kinds.includes(entry.envelope.kind)) {
|
|
120
125
|
return false;
|
|
121
126
|
}
|
|
122
|
-
if (query.tag !== undefined && !entry.
|
|
127
|
+
if (query.tag !== undefined && !entry.envelope.tags.includes(query.tag)) {
|
|
123
128
|
return false;
|
|
124
129
|
}
|
|
125
|
-
if (query.
|
|
130
|
+
if (query.provenanceSource !== undefined && entry.envelope.provenance.source !== query.provenanceSource) {
|
|
126
131
|
return false;
|
|
127
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`.
|
|
128
136
|
return true;
|
|
129
137
|
}
|
|
130
138
|
/**
|
|
131
|
-
* Apply the shared scope / kind / tag / predicate pre-filter
|
|
132
|
-
* entries, returning the surviving records (unordered,
|
|
139
|
+
* Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
|
|
140
|
+
* to a set of indexed entries, returning the surviving records (unordered,
|
|
141
|
+
* unlimited).
|
|
133
142
|
* @public
|
|
134
143
|
*/
|
|
135
144
|
function selectByQuery(entries, query) {
|
|
136
|
-
return entries.filter((entry) => indexedRecordMatchesQuery(entry, query))
|
|
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) : [];
|
|
137
228
|
}
|
|
138
229
|
/**
|
|
139
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, 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 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 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 / predicate\n * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /\n * link axes are NOT applied here — those 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 (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / predicate pre-filter to a set of indexed\n * entries, returning the surviving records (unordered, 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,
|
|
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(
|
|
25
|
-
this._index = index;
|
|
26
|
-
this.
|
|
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
|
|
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
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
71
|
+
const entry = this._index.get(hit.target);
|
|
74
72
|
if (entry !== undefined && (0, retriever_1.indexedRecordMatchesQuery)(entry, query)) {
|
|
75
|
-
|
|
73
|
+
selected.push(entry);
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
|
-
|
|
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":";AAAA;;;GAGG;;;AAEH,4CAAsD;
|
|
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"]}
|