@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.
- 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 +12 -26
- package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/dist/packlets/retrieve/recencyRetriever.js +7 -7
- package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/dist/packlets/retrieve/retriever.js +91 -10
- 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 +7 -7
- package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/dist/packlets/retrieve/tagRetriever.js +7 -7
- package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
- package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
- 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 +221 -79
- 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 +1035 -106
- 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 +3 -10
- package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
- package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
- package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/recencyRetriever.js +6 -6
- package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
- package/lib/packlets/retrieve/retriever.d.ts +88 -7
- package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/retriever.js +94 -9
- 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 +3 -3
- package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
- package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
- package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
- package/lib/packlets/retrieve/tagRetriever.js +6 -6
- 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 +22 -19
- 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 +53 -166
- package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
- package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
- 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 +24 -2
- 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
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.InMemoryFragmentCosineIndex = void 0;
|
|
8
8
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
9
9
|
const types_1 = require("../types");
|
|
10
|
+
const rebuildHelpers_1 = require("./rebuildHelpers");
|
|
10
11
|
/** Project an incoming fragment's identity fields, dropping the ones it did not carry. */
|
|
11
12
|
function fragmentIdentity(fragment) {
|
|
12
13
|
return Object.assign(Object.assign({}, (fragment.locator !== undefined ? { locator: fragment.locator } : {})), (fragment.fragmentId !== undefined ? { fragmentId: fragment.fragmentId } : {}));
|
|
@@ -99,6 +100,10 @@ class InMemoryFragmentCosineIndex {
|
|
|
99
100
|
}
|
|
100
101
|
return Promise.resolve((0, ts_utils_1.succeed)(stored.length));
|
|
101
102
|
}
|
|
103
|
+
/** {@inheritDoc IFragmentVectorIndex.has} */
|
|
104
|
+
has(target) {
|
|
105
|
+
return Promise.resolve((0, ts_utils_1.succeed)(this._records.has((0, types_1.edgeTargetKey)(target))));
|
|
106
|
+
}
|
|
102
107
|
/** {@inheritDoc IFragmentVectorIndex.remove} */
|
|
103
108
|
remove(target) {
|
|
104
109
|
this._records.delete((0, types_1.edgeTargetKey)(target));
|
|
@@ -165,25 +170,75 @@ class InMemoryFragmentCosineIndex {
|
|
|
165
170
|
* @param source - The scope-qualified record source to re-embed.
|
|
166
171
|
* @param embed - The fragment embedder applied to each record.
|
|
167
172
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
/** {@inheritDoc IFragmentVectorIndex.rebuild} */
|
|
174
|
+
async rebuild(source, embed, options) {
|
|
175
|
+
var _a;
|
|
176
|
+
const lenient = ((_a = options === null || options === void 0 ? void 0 : options.onRecordError) !== null && _a !== void 0 ? _a : 'fail') === 'skip';
|
|
177
|
+
const listed = await (0, rebuildHelpers_1.invokeHook)(() => source.list());
|
|
171
178
|
if (listed.isFailure()) {
|
|
172
|
-
|
|
179
|
+
// Deliberately BEFORE the reset, matching the record-granular sibling: a
|
|
180
|
+
// failed list is no evidence about the fragments already held, and nothing
|
|
181
|
+
// has been re-embedded yet, so there is no half-rebuilt state to guard
|
|
182
|
+
// against. Discarding a healthy index over a transient read error is data
|
|
183
|
+
// loss, not caution. No detail either — an all-zero report would describe
|
|
184
|
+
// an index this call never touched.
|
|
185
|
+
return (0, ts_utils_1.failWithDetail)(`fragment index rebuild: failed to list records: ${listed.message}`);
|
|
173
186
|
}
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
// From here a rebuild is genuinely starting, so clear. A mid-loop failure
|
|
188
|
+
// under `'fail'` still resets, which is what keeps that contract honest.
|
|
189
|
+
this._reset();
|
|
190
|
+
const indexed = new Map();
|
|
191
|
+
const fragments = new Map();
|
|
192
|
+
const declined = new Map();
|
|
193
|
+
const skipped = [];
|
|
194
|
+
// Only the source knows what it filtered, so an absent `excluded` propagates
|
|
195
|
+
// as absent rather than becoming an empty map. Before this contract existed
|
|
196
|
+
// the fragment path dropped this tally on the floor, having nowhere honest to
|
|
197
|
+
// put it.
|
|
198
|
+
const report = () => ({
|
|
199
|
+
indexed,
|
|
200
|
+
fragments,
|
|
201
|
+
declined,
|
|
202
|
+
excluded: listed.value.excluded,
|
|
203
|
+
skipped
|
|
204
|
+
});
|
|
205
|
+
for (const scoped of listed.value.records) {
|
|
206
|
+
const kind = scoped.record.envelope.kind;
|
|
207
|
+
// Consumer-supplied, so a throw or rejection is captured rather than
|
|
208
|
+
// escaping mid-loop and leaving the index half-populated.
|
|
209
|
+
const embedded = await (0, rebuildHelpers_1.invokeHook)(() => embed(scoped.record));
|
|
176
210
|
if (embedded.isFailure()) {
|
|
177
|
-
|
|
178
|
-
|
|
211
|
+
const error = `fragment index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`;
|
|
212
|
+
if (!lenient) {
|
|
213
|
+
this._reset();
|
|
214
|
+
return (0, ts_utils_1.failWithDetail)(error, report());
|
|
215
|
+
}
|
|
216
|
+
skipped.push({ target: scoped.target, error });
|
|
217
|
+
continue;
|
|
179
218
|
}
|
|
219
|
+
// An empty array is this lane's decline. It still performs a real
|
|
220
|
+
// whole-record-replace — which is what clears any stale fragments — so it
|
|
221
|
+
// is written, then counted as declined rather than indexed.
|
|
180
222
|
const added = await this.addFragments(scoped.target, embedded.value);
|
|
181
223
|
if (added.isFailure()) {
|
|
182
|
-
|
|
183
|
-
|
|
224
|
+
const error = `fragment index rebuild: ${added.message}`;
|
|
225
|
+
if (!lenient) {
|
|
226
|
+
this._reset();
|
|
227
|
+
return (0, ts_utils_1.failWithDetail)(error, report());
|
|
228
|
+
}
|
|
229
|
+
skipped.push({ target: scoped.target, error });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (added.value === 0) {
|
|
233
|
+
(0, rebuildHelpers_1.tally)(declined, kind);
|
|
234
|
+
continue;
|
|
184
235
|
}
|
|
236
|
+
// Tallied per successful add rather than read back off the counts at the
|
|
237
|
+
// end, so the per-kind buckets line up with their siblings.
|
|
238
|
+
(0, rebuildHelpers_1.tally)(indexed, kind);
|
|
239
|
+
(0, rebuildHelpers_1.tally)(fragments, kind, added.value);
|
|
185
240
|
}
|
|
186
|
-
return (0, ts_utils_1.
|
|
241
|
+
return (0, ts_utils_1.succeedWithDetail)(report());
|
|
187
242
|
}
|
|
188
243
|
/** Empty the index and forget the established dimension. */
|
|
189
244
|
_reset() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inMemoryFragmentCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.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, edgeTargetKey } from '../types';\nimport {\n FragmentEmbedder,\n IEmbeddedFragment,\n IFragmentVectorIndex,\n IMemoryRecordSource,\n IScopedMemoryRecord,\n IVectorQueryHit\n} from './vectorIndex';\n\n/**\n * The identity fields a fragment was added with, already in query-hit shape: a field\n * the fragment did not carry is *absent*, never present-but-`undefined`, so a hit\n * for a fragment added without a `fragmentId` is structurally identical to one\n * produced before `fragmentId` existed.\n */\ntype FragmentIdentity = Pick<IVectorQueryHit, 'locator' | 'fragmentId'>;\n\n/** Project an incoming fragment's identity fields, dropping the ones it did not carry. */\nfunction fragmentIdentity(fragment: IEmbeddedFragment): FragmentIdentity {\n return {\n ...(fragment.locator !== undefined ? { locator: fragment.locator } : {}),\n ...(fragment.fragmentId !== undefined ? { fragmentId: fragment.fragmentId } : {})\n };\n}\n\n/** One stored fragment: the identity it was added with plus its vector. */\ninterface IStoredFragment {\n readonly identity: FragmentIdentity;\n readonly vector: Float32Array;\n}\n\n/** Every stored fragment for one record, tagged with the record's scoped address. */\ninterface IStoredRecordFragments {\n readonly target: IEdgeTarget;\n readonly fragments: IStoredFragment[];\n}\n\n/** A candidate hit carried through selection: the fragment's key, hit, and score. */\ninterface IScoredFragment {\n readonly key: string;\n readonly hit: IVectorQueryHit;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the\n * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many\n * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and\n * answers a query by computing cosine similarity against every stored fragment,\n * returning the top-k fragment hits by descending score. Each hit carries back\n * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was\n * added with; a fragment must carry at least one of the two.\n *\n * @remarks\n * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external\n * dependency, no ANN structure, a linear scan over the stored fragments — the seam\n * ({@link IFragmentVectorIndex}) stays open for a consumer to swap a persistent /\n * ANN backend once N grows. `addFragments` is whole-record-replace, so re-authoring\n * a document never leaves stale fragments behind. The index has a single dimension\n * established by the first fragment added; every subsequent fragment and every\n * `query` vector must match it or fail loudly — a mismatched dimension is an\n * embedder-wiring bug, never a silent zero-similarity result.\n * {@link InMemoryFragmentCosineIndex.rebuild | rebuild} clears the index (and the\n * established dimension), so a re-embed with a different model is supported.\n *\n * The optional `maxPerRecord` cap on `query` is applied **during selection**, before\n * the `topK` cut, so one long document with many strong fragments cannot crowd every\n * other record out of the result.\n * @public\n */\nexport class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {\n /**\n * Stored fragments keyed by the canonical {@link edgeTargetKey} of the record's\n * scope-qualified address, so two records that share a filename stem across\n * scopes occupy distinct entries and never overwrite each other's fragments.\n */\n private readonly _records: Map<string, IStoredRecordFragments>;\n /** The dimension of every stored fragment vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._records = new Map<string, IStoredRecordFragments>();\n this._dimension = undefined;\n }\n\n /** The number of records that currently have at least one stored fragment. */\n public get recordCount(): number {\n return this._records.size;\n }\n\n /** The total number of fragments currently held across all records. */\n public get fragmentCount(): number {\n let total: number = 0;\n for (const record of this._records.values()) {\n total += record.fragments.length;\n }\n return total;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryFragmentCosineIndex> {\n return succeed(new InMemoryFragmentCosineIndex());\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before mutating any state, so a bad fragment never\n // leaves the record half-replaced OR the index dimension half-established\n // (whole-record-replace must be all-or-nothing). The effective dimension is the\n // established one, or — on a still-dimensionless index — the first fragment's\n // length; it is only committed to `this._dimension` once the whole batch passes.\n const stored: IStoredFragment[] = [];\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // A fragment carrying neither identity cannot be resolved back to anything by a\n // consumer holding the hit — the same invariant `embeddedFragmentConverter`\n // enforces at the untyped boundary, re-checked here at the index seam.\n if (fragment.locator === undefined && fragment.fragmentId === undefined) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `addFragments`.\n stored.push({ identity: fragmentIdentity(fragment), vector: Float32Array.from(fragment.vector) });\n }\n // Whole-record replace: an empty `fragments` array drops the record entirely\n // rather than leaving an empty shell behind. Commit the (possibly newly-derived)\n // dimension only alongside a successful, non-empty store.\n if (stored.length === 0) {\n this._records.delete(key);\n } else {\n this._dimension = dimension;\n this._records.set(key, { target, fragments: stored });\n }\n return Promise.resolve(succeed(stored.length));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._records.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._records.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryFragmentCosineIndex._magnitude(vector);\n const scored: IScoredFragment[] = [];\n for (const record of this._records.values()) {\n for (const fragment of record.fragments) {\n scored.push({\n key: edgeTargetKey(record.target),\n hit: {\n target: record.target,\n score: InMemoryFragmentCosineIndex._cosine(vector, queryMagnitude, fragment.vector),\n ...fragment.identity\n }\n });\n }\n }\n // Descending by score; the caller re-resolves each `(target, locator)` hit.\n scored.sort((a, b) => b.hit.score - a.hit.score);\n\n const hits: IVectorQueryHit[] = [];\n // Apply the per-record cap during selection (before the topK cut) so a single\n // long document cannot monopolize the result. `undefined` maxPerRecord means\n // uncapped; the counter map is always allocated (tiny) so the guard narrows\n // `maxPerRecord` directly without a non-null assertion.\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const candidate of scored) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(candidate.key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(candidate.key, used + 1);\n }\n hits.push(candidate.hit);\n }\n return Promise.resolve(succeed(hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the fragment index from\n * scratch. Clears the current contents (and the established dimension) first, so\n * a re-embed with a different model is supported. Returns the total number of\n * fragments indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty rather\n * than left in a partially-rebuilt state.\n *\n * @remarks\n * **Deliberately still returns a bare count**, unlike the record-granular\n * {@link InMemoryCosineIndex.rebuild}, which reports an\n * {@link IVectorRebuildReport}. The asymmetry is scope, not oversight: the\n * fragment path is tracked separately and gains the same treatment when the\n * `IVectorIndex`/`IFragmentVectorIndex` contracts are revisited together.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The fragment embedder applied to each record.\n */\n public async rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder): Promise<Result<number>> {\n this._reset();\n const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await source.list();\n if (listed.isFailure()) {\n return fail(`fragment index rebuild: failed to list records: ${listed.message}`);\n }\n for (const scoped of listed.value) {\n const embedded: Result<ReadonlyArray<IEmbeddedFragment>> = await embed(scoped.record);\n if (embedded.isFailure()) {\n this._reset();\n return fail(\n `fragment index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${embedded.message}`\n );\n }\n const added: Result<number> = await this.addFragments(scoped.target, embedded.value);\n if (added.isFailure()) {\n this._reset();\n return fail(`fragment index rebuild: ${added.message}`);\n }\n }\n return succeed(this.fragmentCount);\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._records.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored fragment vector. A zero-magnitude vector\n * on either side yields `0` rather than `NaN`.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryFragmentCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"inMemoryFragmentCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { DetailedResult, Result, fail, failWithDetail, succeed, succeedWithDetail } from '@fgv/ts-utils';\nimport { IEdgeTarget, Kind, edgeTargetKey } from '../types';\nimport {\n FragmentEmbedder,\n IEmbeddedFragment,\n IFragmentVectorIndex,\n IFragmentVectorRebuildReport,\n IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorQueryHit,\n IVectorRebuildOptions\n} from './vectorIndex';\nimport { invokeHook, tally } from './rebuildHelpers';\n\n/**\n * The identity fields a fragment was added with, already in query-hit shape: a field\n * the fragment did not carry is *absent*, never present-but-`undefined`, so a hit\n * for a fragment added without a `fragmentId` is structurally identical to one\n * produced before `fragmentId` existed.\n */\ntype FragmentIdentity = Pick<IVectorQueryHit, 'locator' | 'fragmentId'>;\n\n/** Project an incoming fragment's identity fields, dropping the ones it did not carry. */\nfunction fragmentIdentity(fragment: IEmbeddedFragment): FragmentIdentity {\n return {\n ...(fragment.locator !== undefined ? { locator: fragment.locator } : {}),\n ...(fragment.fragmentId !== undefined ? { fragmentId: fragment.fragmentId } : {})\n };\n}\n\n/** One stored fragment: the identity it was added with plus its vector. */\ninterface IStoredFragment {\n readonly identity: FragmentIdentity;\n readonly vector: Float32Array;\n}\n\n/** Every stored fragment for one record, tagged with the record's scoped address. */\ninterface IStoredRecordFragments {\n readonly target: IEdgeTarget;\n readonly fragments: IStoredFragment[];\n}\n\n/** A candidate hit carried through selection: the fragment's key, hit, and score. */\ninterface IScoredFragment {\n readonly key: string;\n readonly hit: IVectorQueryHit;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the\n * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many\n * `Float32Array`s per record (one per {@link IEmbeddedFragment | fragment}) and\n * answers a query by computing cosine similarity against every stored fragment,\n * returning the top-k fragment hits by descending score. Each hit carries back\n * whichever of {@link IFragmentLocator | locator} / `fragmentId` its fragment was\n * added with; a fragment must carry at least one of the two.\n *\n * @remarks\n * Same regime and same non-goals as {@link InMemoryCosineIndex}: no external\n * dependency, no ANN structure, a linear scan over the stored fragments — the seam\n * ({@link IFragmentVectorIndex}) stays open for a consumer to swap a persistent /\n * ANN backend once N grows. `addFragments` is whole-record-replace, so re-authoring\n * a document never leaves stale fragments behind. The index has a single dimension\n * established by the first fragment added; every subsequent fragment and every\n * `query` vector must match it or fail loudly — a mismatched dimension is an\n * embedder-wiring bug, never a silent zero-similarity result.\n * {@link InMemoryFragmentCosineIndex.rebuild | rebuild} clears the index (and the\n * established dimension), so a re-embed with a different model is supported.\n *\n * The optional `maxPerRecord` cap on `query` is applied **during selection**, before\n * the `topK` cut, so one long document with many strong fragments cannot crowd every\n * other record out of the result.\n * @public\n */\nexport class InMemoryFragmentCosineIndex implements IFragmentVectorIndex {\n /**\n * Stored fragments keyed by the canonical {@link edgeTargetKey} of the record's\n * scope-qualified address, so two records that share a filename stem across\n * scopes occupy distinct entries and never overwrite each other's fragments.\n */\n private readonly _records: Map<string, IStoredRecordFragments>;\n /** The dimension of every stored fragment vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._records = new Map<string, IStoredRecordFragments>();\n this._dimension = undefined;\n }\n\n /** The number of records that currently have at least one stored fragment. */\n public get recordCount(): number {\n return this._records.size;\n }\n\n /** The total number of fragments currently held across all records. */\n public get fragmentCount(): number {\n let total: number = 0;\n for (const record of this._records.values()) {\n total += record.fragments.length;\n }\n return total;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryFragmentCosineIndex> {\n return succeed(new InMemoryFragmentCosineIndex());\n }\n\n /** {@inheritDoc IFragmentVectorIndex.addFragments} */\n public addFragments(\n target: IEdgeTarget,\n fragments: ReadonlyArray<IEmbeddedFragment>\n ): Promise<Result<number>> {\n const key: string = edgeTargetKey(target);\n // Validate every fragment before mutating any state, so a bad fragment never\n // leaves the record half-replaced OR the index dimension half-established\n // (whole-record-replace must be all-or-nothing). The effective dimension is the\n // established one, or — on a still-dimensionless index — the first fragment's\n // length; it is only committed to `this._dimension` once the whole batch passes.\n const stored: IStoredFragment[] = [];\n let dimension: number | undefined = this._dimension;\n for (const fragment of fragments) {\n if (fragment.vector.length === 0) {\n return Promise.resolve(fail(`fragment index: cannot add '${key}': empty fragment vector`));\n }\n // A fragment carrying neither identity cannot be resolved back to anything by a\n // consumer holding the hit — the same invariant `embeddedFragmentConverter`\n // enforces at the untyped boundary, re-checked here at the index seam.\n if (fragment.locator === undefined && fragment.fragmentId === undefined) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment requires at least one of 'locator' or 'fragmentId'`\n )\n );\n }\n if (dimension === undefined) {\n dimension = fragment.vector.length;\n } else if (fragment.vector.length !== dimension) {\n return Promise.resolve(\n fail(\n `fragment index: cannot add '${key}': fragment dimension ${fragment.vector.length} does not match index dimension ${dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `addFragments`.\n stored.push({ identity: fragmentIdentity(fragment), vector: Float32Array.from(fragment.vector) });\n }\n // Whole-record replace: an empty `fragments` array drops the record entirely\n // rather than leaving an empty shell behind. Commit the (possibly newly-derived)\n // dimension only alongside a successful, non-empty store.\n if (stored.length === 0) {\n this._records.delete(key);\n } else {\n this._dimension = dimension;\n this._records.set(key, { target, fragments: stored });\n }\n return Promise.resolve(succeed(stored.length));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(succeed(this._records.has(edgeTargetKey(target))));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._records.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IFragmentVectorIndex.query} */\n public query(\n vector: Float32Array,\n topK: number,\n maxPerRecord?: number\n ): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._records.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `fragment index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryFragmentCosineIndex._magnitude(vector);\n const scored: IScoredFragment[] = [];\n for (const record of this._records.values()) {\n for (const fragment of record.fragments) {\n scored.push({\n key: edgeTargetKey(record.target),\n hit: {\n target: record.target,\n score: InMemoryFragmentCosineIndex._cosine(vector, queryMagnitude, fragment.vector),\n ...fragment.identity\n }\n });\n }\n }\n // Descending by score; the caller re-resolves each `(target, locator)` hit.\n scored.sort((a, b) => b.hit.score - a.hit.score);\n\n const hits: IVectorQueryHit[] = [];\n // Apply the per-record cap during selection (before the topK cut) so a single\n // long document cannot monopolize the result. `undefined` maxPerRecord means\n // uncapped; the counter map is always allocated (tiny) so the guard narrows\n // `maxPerRecord` directly without a non-null assertion.\n const perRecord: Map<string, number> = new Map<string, number>();\n for (const candidate of scored) {\n if (hits.length >= topK) {\n break;\n }\n if (maxPerRecord !== undefined) {\n const used: number = perRecord.get(candidate.key) ?? 0;\n if (used >= maxPerRecord) {\n continue;\n }\n perRecord.set(candidate.key, used + 1);\n }\n hits.push(candidate.hit);\n }\n return Promise.resolve(succeed(hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the fragment index from\n * scratch. Clears the current contents (and the established dimension) first, so\n * a re-embed with a different model is supported. Returns the total number of\n * fragments indexed.\n *\n * On any failure (list, embed, or add) the index is rolled back to empty rather\n * than left in a partially-rebuilt state.\n *\n * @remarks\n * **Deliberately still returns a bare count**, unlike the record-granular\n * {@link InMemoryCosineIndex.rebuild}, which reports an\n * {@link IVectorRebuildReport}. The asymmetry is scope, not oversight: the\n * fragment path is tracked separately and gains the same treatment when the\n * `IVectorIndex`/`IFragmentVectorIndex` contracts are revisited together.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The fragment embedder applied to each record.\n */\n /** {@inheritDoc IFragmentVectorIndex.rebuild} */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: FragmentEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<IMemoryRecordListing> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset, matching the record-granular sibling: a\n // failed list is no evidence about the fragments already held, and nothing\n // has been re-embedded yet, so there is no half-rebuilt state to guard\n // against. Discarding a healthy index over a transient read error is data\n // loss, not caution. No detail either — an all-zero report would describe\n // an index this call never touched.\n return failWithDetail(`fragment index rebuild: failed to list records: ${listed.message}`);\n }\n // From here a rebuild is genuinely starting, so clear. A mid-loop failure\n // under `'fail'` still resets, which is what keeps that contract honest.\n this._reset();\n const indexed: Map<Kind, number> = new Map<Kind, number>();\n const fragments: Map<Kind, number> = new Map<Kind, number>();\n const declined: Map<Kind, number> = new Map<Kind, number>();\n const skipped: ISkippedVectorRecord[] = [];\n // Only the source knows what it filtered, so an absent `excluded` propagates\n // as absent rather than becoming an empty map. Before this contract existed\n // the fragment path dropped this tally on the floor, having nowhere honest to\n // put it.\n const report = (): IFragmentVectorRebuildReport => ({\n indexed,\n fragments,\n declined,\n excluded: listed.value.excluded,\n skipped\n });\n for (const scoped of listed.value.records) {\n const kind: Kind = scoped.record.envelope.kind;\n // Consumer-supplied, so a throw or rejection is captured rather than\n // escaping mid-loop and leaving the index half-populated.\n const embedded: Result<ReadonlyArray<IEmbeddedFragment>> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `fragment index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // An empty array is this lane's decline. It still performs a real\n // whole-record-replace — which is what clears any stale fragments — so it\n // is written, then counted as declined rather than indexed.\n const added: Result<number> = await this.addFragments(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `fragment index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n if (added.value === 0) {\n tally(declined, kind);\n continue;\n }\n // Tallied per successful add rather than read back off the counts at the\n // end, so the per-kind buckets line up with their siblings.\n tally(indexed, kind);\n tally(fragments, kind, added.value);\n }\n return succeedWithDetail(report());\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._records.clear();\n this._dimension = undefined;\n }\n\n /** The Euclidean magnitude (L2 norm) of a vector. */\n private static _magnitude(vector: Float32Array): number {\n let sum: number = 0;\n for (let i: number = 0; i < vector.length; i++) {\n sum += vector[i] * vector[i];\n }\n return Math.sqrt(sum);\n }\n\n /**\n * Cosine similarity between the query (whose magnitude is precomputed once and\n * reused across the scan) and a stored fragment vector. A zero-magnitude vector\n * on either side yields `0` rather than `NaN`.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryFragmentCosineIndex._magnitude(stored);\n if (queryMagnitude === 0 || storedMagnitude === 0) {\n return 0;\n }\n let dot: number = 0;\n for (let i: number = 0; i < query.length; i++) {\n dot += query[i] * stored[i];\n }\n return dot / (queryMagnitude * storedMagnitude);\n }\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Result } from '@fgv/ts-utils';
|
|
2
|
+
import { Kind } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Invoke a consumer-supplied hook that already returns a `Result`, converting a
|
|
5
|
+
* synchronous throw or a promise rejection into a `Failure` rather than letting
|
|
6
|
+
* it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome
|
|
7
|
+
* is flattened back to one level.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Package-internal, and deliberately not exported from the package surface: both
|
|
11
|
+
* shipped index implementations need it, which is what moved it out of
|
|
12
|
+
* `inMemoryCosineIndex.ts`, but publishing it would invite treating the
|
|
13
|
+
* invocation shape as part of the `IVectorIndex` contract when only the report
|
|
14
|
+
* is.
|
|
15
|
+
*/
|
|
16
|
+
export declare function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>>;
|
|
17
|
+
/**
|
|
18
|
+
* Increment `kind`'s tally by `by` (default one).
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Package-internal for the same reason as {@link invokeHook}: publishing a
|
|
22
|
+
* mutation primitive would buy nothing a caller could not write, and would invite
|
|
23
|
+
* treating the accumulation shape as contractual.
|
|
24
|
+
*
|
|
25
|
+
* The `by` parameter exists for the fragment lane, whose `fragments` count
|
|
26
|
+
* accumulates a fan-out rather than a record count — the one place a rebuild adds
|
|
27
|
+
* more than one per record.
|
|
28
|
+
*/
|
|
29
|
+
export declare function tally(counts: Map<Kind, number>, kind: Kind, by?: number): void;
|
|
30
|
+
//# sourceMappingURL=rebuildHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebuildHelpers.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/rebuildHelpers.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAsB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAEtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,GAAE,MAAU,GAAG,IAAI,CAEjF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Erik Fortune
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.invokeHook = invokeHook;
|
|
8
|
+
exports.tally = tally;
|
|
9
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
10
|
+
/**
|
|
11
|
+
* Invoke a consumer-supplied hook that already returns a `Result`, converting a
|
|
12
|
+
* synchronous throw or a promise rejection into a `Failure` rather than letting
|
|
13
|
+
* it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome
|
|
14
|
+
* is flattened back to one level.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Package-internal, and deliberately not exported from the package surface: both
|
|
18
|
+
* shipped index implementations need it, which is what moved it out of
|
|
19
|
+
* `inMemoryCosineIndex.ts`, but publishing it would invite treating the
|
|
20
|
+
* invocation shape as part of the `IVectorIndex` contract when only the report
|
|
21
|
+
* is.
|
|
22
|
+
*/
|
|
23
|
+
async function invokeHook(hook) {
|
|
24
|
+
return (await (0, ts_utils_1.captureAsyncResult)(hook)).onSuccess((inner) => inner);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Increment `kind`'s tally by `by` (default one).
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Package-internal for the same reason as {@link invokeHook}: publishing a
|
|
31
|
+
* mutation primitive would buy nothing a caller could not write, and would invite
|
|
32
|
+
* treating the accumulation shape as contractual.
|
|
33
|
+
*
|
|
34
|
+
* The `by` parameter exists for the fragment lane, whose `fragments` count
|
|
35
|
+
* accumulates a fan-out rather than a record count — the one place a rebuild adds
|
|
36
|
+
* more than one per record.
|
|
37
|
+
*/
|
|
38
|
+
function tally(counts, kind, by = 1) {
|
|
39
|
+
var _a;
|
|
40
|
+
counts.set(kind, ((_a = counts.get(kind)) !== null && _a !== void 0 ? _a : 0) + by);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=rebuildHelpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebuildHelpers.js","sourceRoot":"","sources":["../../../src/packlets/vector/rebuildHelpers.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAkBH,gCAEC;AAcD,sBAEC;AAlCD,4CAA2D;AAG3D;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,UAAU,CAAI,IAA8B;IAChE,OAAO,CAAC,MAAM,IAAA,6BAAkB,EAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,KAAK,CAAC,MAAyB,EAAE,IAAU,EAAE,KAAa,CAAC;;IACzE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAA,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AACjD,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureAsyncResult } from '@fgv/ts-utils';\nimport { Kind } from '../types';\n\n/**\n * Invoke a consumer-supplied hook that already returns a `Result`, converting a\n * synchronous throw or a promise rejection into a `Failure` rather than letting\n * it escape. `captureAsyncResult` wraps the hook's own `Result`, so the outcome\n * is flattened back to one level.\n *\n * @remarks\n * Package-internal, and deliberately not exported from the package surface: both\n * shipped index implementations need it, which is what moved it out of\n * `inMemoryCosineIndex.ts`, but publishing it would invite treating the\n * invocation shape as part of the `IVectorIndex` contract when only the report\n * is.\n */\nexport async function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>> {\n return (await captureAsyncResult(hook)).onSuccess((inner) => inner);\n}\n\n/**\n * Increment `kind`'s tally by `by` (default one).\n *\n * @remarks\n * Package-internal for the same reason as {@link invokeHook}: publishing a\n * mutation primitive would buy nothing a caller could not write, and would invite\n * treating the accumulation shape as contractual.\n *\n * The `by` parameter exists for the fragment lane, whose `fragments` count\n * accumulates a fan-out rather than a record count — the one place a rebuild adds\n * more than one per record.\n */\nexport function tally(counts: Map<Kind, number>, kind: Kind, by: number = 1): void {\n counts.set(kind, (counts.get(kind) ?? 0) + by);\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import { IEdgeTarget, IMemoryRecord } from '../types';
|
|
1
|
+
import { DetailedResult, Result } from '@fgv/ts-utils';
|
|
2
|
+
import { IEdgeTarget, IMemoryRecord, Kind } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* A half-open `[start, end)` span into a record's body — the in-record locator a
|
|
5
5
|
* {@link IFragmentVectorIndex} may carry on a fragment. `start` is inclusive,
|
|
@@ -117,6 +117,32 @@ export interface IVectorIndex {
|
|
|
117
117
|
* Return the `topK` nearest records to `vector`, in descending score order.
|
|
118
118
|
*/
|
|
119
119
|
query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
|
|
120
|
+
/**
|
|
121
|
+
* Whether this index holds a vector for the scope-qualified `target`.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* On the contract because it is what makes a **targeted** repair possible, and
|
|
125
|
+
* the reason is sharper than convenience: the only other way to ask *"is this
|
|
126
|
+
* record indexed?"* is {@link IMemoryEnvelope.embeddingRef} — and that field is
|
|
127
|
+
* the store's **belief**, which is wrong in precisely the situation a repair
|
|
128
|
+
* runs in. A reopened vault backed by a fresh in-memory index carries an
|
|
129
|
+
* `embeddingRef` on every record while holding no vectors at all. **A repair
|
|
130
|
+
* that trusts the field it is repairing is not a repair.**
|
|
131
|
+
*
|
|
132
|
+
* It also makes a case *detectable* that an `embeddingRef`-only check cannot
|
|
133
|
+
* see at all: the index holds the vector but the envelope lost its reference
|
|
134
|
+
* (a failure swallowed after the vector was committed). That record needs its
|
|
135
|
+
* reference restamped and **no embedder call**, which is only knowable by
|
|
136
|
+
* asking the index.
|
|
137
|
+
*
|
|
138
|
+
* `Promise<Result<boolean>>`, unlike the synchronous {@link IVectorIndex.size},
|
|
139
|
+
* because the two are not the same kind of accessor and should not be made to
|
|
140
|
+
* look alike: `size` is a count both shipped implementations hold or can read
|
|
141
|
+
* without a failure mode, while `has` on a durable backend is a keyed query
|
|
142
|
+
* that can fail. Idempotent and side-effect-free; an absent target is
|
|
143
|
+
* `succeed(false)`, never a failure.
|
|
144
|
+
*/
|
|
145
|
+
has(target: IEdgeTarget): Promise<Result<boolean>>;
|
|
120
146
|
/**
|
|
121
147
|
* The number of vectors currently held.
|
|
122
148
|
*
|
|
@@ -154,8 +180,36 @@ export interface IVectorIndex {
|
|
|
154
180
|
*
|
|
155
181
|
* See {@link IVectorRebuildReport} for what it reports and
|
|
156
182
|
* {@link IVectorRebuildOptions} for the failure mode.
|
|
183
|
+
*
|
|
184
|
+
* **A failure carries the report too, on the `detail`** — coverage is most
|
|
185
|
+
* wanted exactly when a rebuild did not complete, and withholding it there made
|
|
186
|
+
* the answer depend on the error-handling mode rather than on the question. The
|
|
187
|
+
* `'fail'` contract itself is unchanged: it still resets, still aborts, still
|
|
188
|
+
* returns a failure. It simply also says what it had established before it
|
|
189
|
+
* stopped. See {@link IVectorRebuildReport} for how to read a report that
|
|
190
|
+
* arrived on a failure — it describes the attempt, not the surviving index.
|
|
191
|
+
*
|
|
192
|
+
* On success the report is the **value** — that is where it belongs, and the
|
|
193
|
+
* `detail` is not also populated. A failure carries no report when nothing was
|
|
194
|
+
* attempted: always a `source.list()` failure, and additionally whatever
|
|
195
|
+
* pre-loop step an implementation needs before it can start (the durable
|
|
196
|
+
* `SqliteVecVectorIndex` must clear its table, and a failure to do so is such a
|
|
197
|
+
* case). Those leave the existing index untouched, so an all-zero report would
|
|
198
|
+
* describe an index the call never disturbed.
|
|
199
|
+
*
|
|
200
|
+
* **A rebuild does NOT re-establish the vector dimension on a persistent
|
|
201
|
+
* index, and this is the one place the two shipped implementations genuinely
|
|
202
|
+
* differ.** The in-memory indexes forget their dimension when they reset, so a
|
|
203
|
+
* rebuild with a different-dimension embedder simply re-establishes it. A
|
|
204
|
+
* `vec0`-backed index cannot: its reset is a `DELETE FROM`, the table's
|
|
205
|
+
* declared dimension is part of its schema, and `vec0` has no `ALTER TABLE`.
|
|
206
|
+
* So a rebuild that changes dimension **succeeds in memory and fails on
|
|
207
|
+
* SQLite**, where it needs the same drop-and-re-index the package README
|
|
208
|
+
* prescribes for any schema change (drop the table, or point the index at a
|
|
209
|
+
* fresh `tableName`). Only embedding time is at risk — vectors are derived and
|
|
210
|
+
* the vault records remain authoritative.
|
|
157
211
|
*/
|
|
158
|
-
rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<
|
|
212
|
+
rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>>;
|
|
159
213
|
}
|
|
160
214
|
/**
|
|
161
215
|
* One embedded fragment of a record: the fragment's vector, plus at least one of the
|
|
@@ -240,6 +294,113 @@ export interface IFragmentVectorIndex {
|
|
|
240
294
|
* `topK` cut) so one long document cannot crowd out others.
|
|
241
295
|
*/
|
|
242
296
|
query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
|
|
297
|
+
/**
|
|
298
|
+
* Whether this index holds **any** fragment for the scope-qualified `target`.
|
|
299
|
+
*
|
|
300
|
+
* @remarks
|
|
301
|
+
* The record-granular rationale on {@link IVectorIndex.has} applies verbatim —
|
|
302
|
+
* a repair that trusts `embeddingRef` is not a repair. Note the granularity
|
|
303
|
+
* this deliberately does **not** offer: it answers *"is this record
|
|
304
|
+
* represented?"*, not *"is this particular fragment present?"*. Fragment
|
|
305
|
+
* writes are whole-record-replace, so a record is either represented by the
|
|
306
|
+
* current fragment set or not represented at all; a per-fragment membership
|
|
307
|
+
* check would imply an incremental write path that does not exist.
|
|
308
|
+
*/
|
|
309
|
+
has(target: IEdgeTarget): Promise<Result<boolean>>;
|
|
310
|
+
/**
|
|
311
|
+
* The number of **records** with at least one fragment held.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* Deliberately **not** named `size`, unlike {@link IVectorIndex.size}. This
|
|
315
|
+
* index is one-to-many, so `size` has two defensible readings and a reader
|
|
316
|
+
* arriving from the record-granular sibling — where `size` counts vectors —
|
|
317
|
+
* would take the wrong one silently. Two explicitly-named counts cost one extra
|
|
318
|
+
* member and cannot be misread.
|
|
319
|
+
*/
|
|
320
|
+
readonly recordCount: number;
|
|
321
|
+
/**
|
|
322
|
+
* The total number of **fragments** held across all records.
|
|
323
|
+
*
|
|
324
|
+
* @remarks
|
|
325
|
+
* The fan-out, and the number a caller actually watches: fragments-per-record
|
|
326
|
+
* is what makes a fragment reconcile expensive, and neither `recordCount` nor a
|
|
327
|
+
* record-granular count answers it.
|
|
328
|
+
*/
|
|
329
|
+
readonly fragmentCount: number;
|
|
330
|
+
/**
|
|
331
|
+
* Re-embed every record from `source` and rebuild the fragment index from
|
|
332
|
+
* scratch — the **backfill / reconcile** operation, sibling to
|
|
333
|
+
* {@link IVectorIndex.rebuild}.
|
|
334
|
+
*
|
|
335
|
+
* @remarks
|
|
336
|
+
* On the contract for exactly the reasons its record-granular sibling is, and
|
|
337
|
+
* the fragment lane was worse off: `rebuild` existed only on the bundled
|
|
338
|
+
* in-memory class, and the durable `SqliteVecFragmentIndex` had **no backfill
|
|
339
|
+
* at all**, so a persistent fragment index could not be reconciled by any
|
|
340
|
+
* route — contractual or concrete. Records written while it was unwired, a
|
|
341
|
+
* re-embed after a segmenter change, and reconciliation after a swallowed
|
|
342
|
+
* fragment-embed failure were all unreachable.
|
|
343
|
+
*
|
|
344
|
+
* Semantics are kept observably identical to the record-granular sibling so a
|
|
345
|
+
* caller who has learned one has learned both: a `source.list()` failure is
|
|
346
|
+
* fatal and carries no detail (nothing was attempted, and the existing index is
|
|
347
|
+
* untouched); a genuine rebuild then resets first; `onRecordError` defaults to
|
|
348
|
+
* `'fail'`; and a failure carries whatever the attempt had established on the
|
|
349
|
+
* `detail`.
|
|
350
|
+
*
|
|
351
|
+
* **A rebuild does NOT re-establish the vector dimension on a persistent
|
|
352
|
+
* index, and this is the one place the two shipped implementations genuinely
|
|
353
|
+
* differ.** The in-memory indexes forget their dimension when they reset, so a
|
|
354
|
+
* rebuild with a different-dimension embedder simply re-establishes it. A
|
|
355
|
+
* `vec0`-backed index cannot: its reset is a `DELETE FROM`, the table's
|
|
356
|
+
* declared dimension is part of its schema, and `vec0` has no `ALTER TABLE`.
|
|
357
|
+
* So a rebuild that changes dimension **succeeds in memory and fails on
|
|
358
|
+
* SQLite**, where it needs the same drop-and-re-index the package README
|
|
359
|
+
* prescribes for any schema change (drop the table, or point the index at a
|
|
360
|
+
* fresh `tableName`). Only embedding time is at risk — vectors are derived and
|
|
361
|
+
* the vault records remain authoritative.
|
|
362
|
+
*/
|
|
363
|
+
rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* What an {@link IFragmentVectorIndex.rebuild} established, resolved by
|
|
367
|
+
* {@link Kind} — the fragment-granular sibling of {@link IVectorRebuildReport}.
|
|
368
|
+
*
|
|
369
|
+
* @remarks
|
|
370
|
+
* The rule stated on {@link IVectorRebuildReport} applies here verbatim and is
|
|
371
|
+
* not re-opened: **every count is resolved by kind unless there is a stated
|
|
372
|
+
* reason it cannot be**, `excluded` is optional because only the source can know
|
|
373
|
+
* it, and the report is carried on a failure as well as a success.
|
|
374
|
+
*
|
|
375
|
+
* The one member with no record-granular analogue is
|
|
376
|
+
* {@link IFragmentVectorRebuildReport.fragments | fragments} — the fan-out. It is
|
|
377
|
+
* the number that distinguishes this lane: `indexed: 40` says forty records are
|
|
378
|
+
* represented and says nothing about whether that cost forty embedding round
|
|
379
|
+
* trips or four thousand, which is the difference between a reconcile that
|
|
380
|
+
* finishes in a second and one that blocks a request past thirty.
|
|
381
|
+
*
|
|
382
|
+
* A **declined** record is one whose {@link FragmentEmbedder} returned an empty
|
|
383
|
+
* array — the fragment lane's way of saying *intentionally not embedded*. Note
|
|
384
|
+
* this is a different mechanism from a {@link MemoryEmbedder} decline: an empty
|
|
385
|
+
* array still performs a real whole-record-replace (which is what clears any
|
|
386
|
+
* stale fragments), where a record-granular decline skips the index entirely.
|
|
387
|
+
* @public
|
|
388
|
+
*/
|
|
389
|
+
export interface IFragmentVectorRebuildReport {
|
|
390
|
+
/** Records that ended with at least one fragment held, per kind. */
|
|
391
|
+
readonly indexed: ReadonlyMap<Kind, number>;
|
|
392
|
+
/** Fragments held, per kind — the fan-out `indexed` cannot express. */
|
|
393
|
+
readonly fragments: ReadonlyMap<Kind, number>;
|
|
394
|
+
/** Records whose embedder intentionally produced no fragments, per kind. */
|
|
395
|
+
readonly declined: ReadonlyMap<Kind, number>;
|
|
396
|
+
/**
|
|
397
|
+
* Records the source filtered out before the rebuild saw them, per kind.
|
|
398
|
+
* `undefined` means *this source does not report exclusions*; an empty map
|
|
399
|
+
* means *it does, and excluded nothing*.
|
|
400
|
+
*/
|
|
401
|
+
readonly excluded?: ReadonlyMap<Kind, number>;
|
|
402
|
+
/** Records that failed, per record, with the error — a fault, never a decline. */
|
|
403
|
+
readonly skipped: ReadonlyArray<ISkippedVectorRecord>;
|
|
243
404
|
}
|
|
244
405
|
/**
|
|
245
406
|
* How a vector-index rebuild treats a record it cannot index — whether the
|
|
@@ -277,22 +438,83 @@ export interface ISkippedVectorRecord {
|
|
|
277
438
|
* What a rebuild actually did — the structural answer to "is this index complete?".
|
|
278
439
|
*
|
|
279
440
|
* @remarks
|
|
280
|
-
* A bare count cannot distinguish the
|
|
281
|
-
*
|
|
282
|
-
* `skipped` was a fault, and
|
|
283
|
-
* deriving coverage from a count alone
|
|
284
|
-
* deliberate policy, which is precisely the
|
|
441
|
+
* A bare count cannot distinguish the ways a record can be absent from the index,
|
|
442
|
+
* and that distinction is the entire point: **`declined` was intentional,
|
|
443
|
+
* `excluded` was never offered, `skipped` was a fault, and none of them is the
|
|
444
|
+
* same as "never attempted"**. A caller deriving coverage from a count alone
|
|
445
|
+
* cannot tell an embedder outage from a deliberate policy, which is precisely the
|
|
446
|
+
* confusion this type exists to end.
|
|
447
|
+
*
|
|
448
|
+
* **Every count in this report is resolved by kind.** A coverage report exists to
|
|
449
|
+
* answer *"is my coverage what I intended?"*, and a bare total cannot:
|
|
450
|
+
* `indexed: 500` reads identically whether the right kinds were indexed or a
|
|
451
|
+
* policy drift silently redirected coverage, and the same is true of every other
|
|
452
|
+
* count here. Totals are derivable by summing; the per-kind breakdown is not
|
|
453
|
+
* derivable from anything else — {@link IVectorQueryHit} carries no `kind`,
|
|
454
|
+
* {@link IVectorIndex.query} answers "what is near this" rather than "what is in
|
|
455
|
+
* here", and {@link IVectorIndex.size} is a scalar, so the index cannot be
|
|
456
|
+
* interrogated after the fact for any of them. **A new count added to this report
|
|
457
|
+
* is resolved by kind unless there is a stated reason it cannot be.**
|
|
458
|
+
*
|
|
459
|
+
* `indexed` is the count most tempting to leave bare and the most dangerous to,
|
|
460
|
+
* because it is the number a coverage surface actually renders: 500 bookkeeping
|
|
461
|
+
* rows and zero knowledge rows is a healthy-looking number for a catastrophically
|
|
462
|
+
* broken index.
|
|
463
|
+
*
|
|
464
|
+
* **Reading a report that arrived on a failure.** Under
|
|
465
|
+
* {@link VectorRebuildErrorMode | `onRecordError: 'fail'`} the report is handed
|
|
466
|
+
* back on the failure's `detail` — *after* the rollback has already run. It
|
|
467
|
+
* describes the attempt, not the surviving index: `indexed` names what had been
|
|
468
|
+
* established when the rebuild stopped, and the index itself now holds nothing. It
|
|
469
|
+
* is a diagnostic ("we were 340 knowledge rows in when the embedder died"), not a
|
|
470
|
+
* coverage statement. Only a report from a **successful** rebuild describes what
|
|
471
|
+
* the index holds.
|
|
285
472
|
* @public
|
|
286
473
|
*/
|
|
287
474
|
export interface IVectorRebuildReport {
|
|
288
|
-
/**
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Records embedded and added to the index, counted by {@link Kind}.
|
|
477
|
+
*
|
|
478
|
+
* @remarks
|
|
479
|
+
* A count of successful `add` calls, so it lines up with its per-record
|
|
480
|
+
* siblings and the buckets sum back to the listing. It is deliberately **not**
|
|
481
|
+
* read back off {@link IVectorIndex.size} at the end, which no implementation
|
|
482
|
+
* could resolve by kind anyway. The trade that makes: a `source` that lists the
|
|
483
|
+
* same `(scope, id)` twice contributes twice here while the index holds one
|
|
484
|
+
* vector, where a size read would have self-corrected. A source that does that
|
|
485
|
+
* is malformed, and a total that silently disagreed with the per-kind
|
|
486
|
+
* breakdown would be the worse failure.
|
|
487
|
+
*/
|
|
488
|
+
readonly indexed: ReadonlyMap<Kind, number>;
|
|
489
|
+
/**
|
|
490
|
+
* Records the embedder deliberately declined (resolved `undefined`), counted by
|
|
491
|
+
* {@link Kind}. The embedder was called and answered — contrast `excluded`,
|
|
492
|
+
* where it never was.
|
|
493
|
+
*/
|
|
494
|
+
readonly declined: ReadonlyMap<Kind, number>;
|
|
495
|
+
/**
|
|
496
|
+
* Records the `source` filtered out before the rebuild ever saw them, counted by
|
|
497
|
+
* {@link Kind} — for a store-backed source, the kinds outside
|
|
498
|
+
* {@link IMemoryStore.embedsKind | embedsKind}.
|
|
499
|
+
*
|
|
500
|
+
* **Optional, and the optionality is semantic rather than cosmetic**: it is the
|
|
501
|
+
* one count a rebuild genuinely cannot know for itself, because the decision is
|
|
502
|
+
* made upstream in the source. `undefined` means *this source does not report
|
|
503
|
+
* exclusions* — distinct from an empty map, which means *this source reports
|
|
504
|
+
* them and excluded nothing*. `indexed` and `declined` are knowable by
|
|
505
|
+
* construction (the rebuild either added the vector or the embedder answered)
|
|
506
|
+
* and so are never optional.
|
|
507
|
+
*/
|
|
508
|
+
readonly excluded?: ReadonlyMap<Kind, number>;
|
|
292
509
|
/**
|
|
293
510
|
* Records whose embedding or add FAILED and were skipped. Non-empty only under
|
|
294
511
|
* {@link VectorRebuildErrorMode | `onRecordError: 'skip'`} — under `'fail'` the
|
|
295
|
-
* first failure aborts the rebuild
|
|
512
|
+
* first failure aborts the rebuild, so a `'fail'` report names the casualty in
|
|
513
|
+
* its failure message rather than here.
|
|
514
|
+
*
|
|
515
|
+
* Per-record and carrying the error, so it already implies the per-kind
|
|
516
|
+
* breakdown the counts above spell out; that is the stated reason this one field
|
|
517
|
+
* is not a `ReadonlyMap<Kind, number>`.
|
|
296
518
|
*/
|
|
297
519
|
readonly skipped: ReadonlyArray<ISkippedVectorRecord>;
|
|
298
520
|
}
|
|
@@ -361,6 +583,35 @@ export interface IScopedMemoryRecord {
|
|
|
361
583
|
/** The record itself, passed to the embedder. */
|
|
362
584
|
readonly record: IMemoryRecord<unknown>;
|
|
363
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* What a {@link IMemoryRecordSource.list} call yields: the records the rebuild
|
|
588
|
+
* should embed, plus — when the source can say — what it filtered out on the way.
|
|
589
|
+
*
|
|
590
|
+
* @remarks
|
|
591
|
+
* The exclusion count originates here because **this is the layer where the
|
|
592
|
+
* decision is made**. A rebuild never sees an excluded record, so it cannot count
|
|
593
|
+
* one; a report assembled without this would silently undercount coverage, and
|
|
594
|
+
* undercount in the direction of looking healthier.
|
|
595
|
+
*
|
|
596
|
+
* A store accessor answering "how many are excluded right now" was considered and
|
|
597
|
+
* declined: it answers a *different question* than the report does — "excluded
|
|
598
|
+
* right now" versus "excluded in this reconcile" — and the two legitimately differ
|
|
599
|
+
* whenever records are written between reconciles. Two correct-and-unequal numbers
|
|
600
|
+
* are worse than one absent number: they invite treating a real difference as a
|
|
601
|
+
* bug, or picking whichever supports the conclusion already held.
|
|
602
|
+
* @public
|
|
603
|
+
*/
|
|
604
|
+
export interface IMemoryRecordListing {
|
|
605
|
+
/** Every record the rebuild should embed, each paired with its scoped address. */
|
|
606
|
+
readonly records: ReadonlyArray<IScopedMemoryRecord>;
|
|
607
|
+
/**
|
|
608
|
+
* Records this source filtered out, counted by {@link Kind}. Omit it entirely if
|
|
609
|
+
* the source does not track exclusions — that reads as *"cannot say"* on
|
|
610
|
+
* {@link IVectorRebuildReport.excluded}, which is distinct from an empty map
|
|
611
|
+
* (*"nothing was excluded"*).
|
|
612
|
+
*/
|
|
613
|
+
readonly excluded?: ReadonlyMap<Kind, number>;
|
|
614
|
+
}
|
|
364
615
|
/**
|
|
365
616
|
* The minimal record-source surface {@link InMemoryCosineIndex.rebuild} reads to
|
|
366
617
|
* re-embed an entire vault. Each entry carries the record's scope-qualified
|
|
@@ -372,7 +623,11 @@ export interface IScopedMemoryRecord {
|
|
|
372
623
|
* @public
|
|
373
624
|
*/
|
|
374
625
|
export interface IMemoryRecordSource {
|
|
375
|
-
/**
|
|
376
|
-
|
|
626
|
+
/**
|
|
627
|
+
* List every record the rebuild should embed, each paired with its scoped
|
|
628
|
+
* address, plus the exclusions this source applied if it tracks them. See
|
|
629
|
+
* {@link IMemoryRecordListing}.
|
|
630
|
+
*/
|
|
631
|
+
list(): Promise<Result<IMemoryRecordListing>>;
|
|
377
632
|
}
|
|
378
633
|
//# sourceMappingURL=vectorIndex.d.ts.map
|