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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/packlets/index/memoryIndex.js +23 -16
  2. package/dist/packlets/index/memoryIndex.js.map +1 -1
  3. package/dist/packlets/ingest/orchestrator.js +13 -1
  4. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  5. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  6. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  8. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/retriever.js +91 -10
  10. package/dist/packlets/retrieve/retriever.js.map +1 -1
  11. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  12. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  13. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  14. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  16. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  18. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  19. package/dist/packlets/store/coverage.js +6 -0
  20. package/dist/packlets/store/coverage.js.map +1 -0
  21. package/dist/packlets/store/fileTreeMemoryStore.js +221 -79
  22. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  23. package/dist/packlets/store/index.js +4 -0
  24. package/dist/packlets/store/index.js.map +1 -1
  25. package/dist/packlets/store/listSelection.js +36 -0
  26. package/dist/packlets/store/listSelection.js.map +1 -0
  27. package/dist/packlets/store/memoryStore.js +6 -0
  28. package/dist/packlets/store/memoryStore.js.map +1 -0
  29. package/dist/packlets/store/reconcile.js +6 -0
  30. package/dist/packlets/store/reconcile.js.map +1 -0
  31. package/dist/packlets/store/storeCoverage.js +102 -0
  32. package/dist/packlets/store/storeCoverage.js.map +1 -0
  33. package/dist/packlets/store/storeReconcile.js +122 -0
  34. package/dist/packlets/store/storeReconcile.js.map +1 -0
  35. package/dist/packlets/store/vectorMaintenance.js +116 -8
  36. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  37. package/dist/packlets/store/vectorRecordSource.js +44 -0
  38. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  39. package/dist/packlets/tools/memoryTools.js +25 -2
  40. package/dist/packlets/tools/memoryTools.js.map +1 -1
  41. package/dist/packlets/types/envelope.js +25 -0
  42. package/dist/packlets/types/envelope.js.map +1 -1
  43. package/dist/packlets/types/index.js +1 -0
  44. package/dist/packlets/types/index.js.map +1 -1
  45. package/dist/packlets/types/recordResolver.js +6 -0
  46. package/dist/packlets/types/recordResolver.js.map +1 -0
  47. package/dist/packlets/types/temporal.js.map +1 -1
  48. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  49. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  50. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +67 -12
  51. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  52. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  53. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  54. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  55. package/dist/ts-agent-memory.d.ts +1035 -106
  56. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  57. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  58. package/lib/packlets/index/memoryIndex.js +23 -16
  59. package/lib/packlets/index/memoryIndex.js.map +1 -1
  60. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  61. package/lib/packlets/ingest/orchestrator.js +13 -1
  62. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  63. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  64. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  65. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  66. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  67. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  68. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  69. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  70. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  71. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  72. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  73. package/lib/packlets/retrieve/retriever.js +94 -9
  74. package/lib/packlets/retrieve/retriever.js.map +1 -1
  75. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  76. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  77. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  78. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  79. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  80. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  81. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  82. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  83. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  84. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  85. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  86. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  87. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  88. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  90. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  91. package/lib/packlets/store/coverage.d.ts +102 -0
  92. package/lib/packlets/store/coverage.d.ts.map +1 -0
  93. package/lib/packlets/store/coverage.js +7 -0
  94. package/lib/packlets/store/coverage.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +53 -166
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +221 -79
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/store/index.d.ts +4 -0
  100. package/lib/packlets/store/index.d.ts.map +1 -1
  101. package/lib/packlets/store/index.js +4 -0
  102. package/lib/packlets/store/index.js.map +1 -1
  103. package/lib/packlets/store/listSelection.d.ts +101 -0
  104. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  105. package/lib/packlets/store/listSelection.js +40 -0
  106. package/lib/packlets/store/listSelection.js.map +1 -0
  107. package/lib/packlets/store/memoryStore.d.ts +237 -0
  108. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  109. package/lib/packlets/store/memoryStore.js +7 -0
  110. package/lib/packlets/store/memoryStore.js.map +1 -0
  111. package/lib/packlets/store/reconcile.d.ts +82 -0
  112. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  113. package/lib/packlets/store/reconcile.js +7 -0
  114. package/lib/packlets/store/reconcile.js.map +1 -0
  115. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  116. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  117. package/lib/packlets/store/storeCoverage.js +105 -0
  118. package/lib/packlets/store/storeCoverage.js.map +1 -0
  119. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  120. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  121. package/lib/packlets/store/storeReconcile.js +125 -0
  122. package/lib/packlets/store/storeReconcile.js.map +1 -0
  123. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  124. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  125. package/lib/packlets/store/vectorMaintenance.js +117 -8
  126. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  127. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  128. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  129. package/lib/packlets/store/vectorRecordSource.js +47 -0
  130. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  131. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  132. package/lib/packlets/tools/memoryTools.js +25 -2
  133. package/lib/packlets/tools/memoryTools.js.map +1 -1
  134. package/lib/packlets/types/envelope.d.ts +24 -2
  135. package/lib/packlets/types/envelope.d.ts.map +1 -1
  136. package/lib/packlets/types/envelope.js +26 -0
  137. package/lib/packlets/types/envelope.js.map +1 -1
  138. package/lib/packlets/types/index.d.ts +1 -0
  139. package/lib/packlets/types/index.d.ts.map +1 -1
  140. package/lib/packlets/types/index.js +1 -0
  141. package/lib/packlets/types/index.js.map +1 -1
  142. package/lib/packlets/types/recordResolver.d.ts +39 -0
  143. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  144. package/lib/packlets/types/recordResolver.js +7 -0
  145. package/lib/packlets/types/recordResolver.js.map +1 -0
  146. package/lib/packlets/types/temporal.d.ts +26 -6
  147. package/lib/packlets/types/temporal.d.ts.map +1 -1
  148. package/lib/packlets/types/temporal.js.map +1 -1
  149. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  150. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  151. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  152. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  153. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +6 -3
  154. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  155. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +66 -11
  156. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  157. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  158. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  159. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  160. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  161. package/lib/packlets/vector/vectorIndex.d.ts +270 -15
  162. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  163. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  164. package/package.json +7 -7
@@ -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
- async rebuild(source, embed) {
169
- this._reset();
170
- const listed = await source.list();
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
- return (0, ts_utils_1.fail)(`fragment index rebuild: failed to list records: ${listed.message}`);
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
- for (const scoped of listed.value) {
175
- const embedded = await embed(scoped.record);
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
- this._reset();
178
- return (0, ts_utils_1.fail)(`fragment index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`);
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
- this._reset();
183
- return (0, ts_utils_1.fail)(`fragment index rebuild: ${added.message}`);
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.succeed)(this.fragmentCount);
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":";AAAA;;;GAGG;;;AAEH,4CAAyG;AACzG,oCAA4D;AAY5D,qDAAqD;AAUrD,0FAA0F;AAC1F,SAAS,gBAAgB,CAAC,QAA2B;IACnD,uCACK,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GACrE,CAAC,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACjF;AACJ,CAAC;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAa,2BAA2B;IAUtC;QACE,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAC;QAC1D,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,8EAA8E;IAC9E,IAAW,WAAW;QACpB,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,IAAW,aAAa;QACtB,IAAI,KAAK,GAAW,CAAC,CAAC;QACtB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,KAAK,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QACnC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM;QAClB,OAAO,IAAA,kBAAO,EAAC,IAAI,2BAA2B,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,sDAAsD;IAC/C,YAAY,CACjB,MAAmB,EACnB,SAA2C;QAE3C,MAAM,GAAG,GAAW,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC;QAC1C,6EAA6E;QAC7E,0EAA0E;QAC1E,gFAAgF;QAChF,8EAA8E;QAC9E,iFAAiF;QACjF,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,IAAI,SAAS,GAAuB,IAAI,CAAC,UAAU,CAAC;QACpD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,eAAI,EAAC,+BAA+B,GAAG,0BAA0B,CAAC,CAAC,CAAC;YAC7F,CAAC;YACD,gFAAgF;YAChF,4EAA4E;YAC5E,uEAAuE;YACvE,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;gBACxE,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,eAAI,EACF,+BAA+B,GAAG,gEAAgE,CACnG,CACF,CAAC;YACJ,CAAC;YACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YACrC,CAAC;iBAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAChD,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,eAAI,EACF,+BAA+B,GAAG,yBAAyB,QAAQ,CAAC,MAAM,CAAC,MAAM,mCAAmC,SAAS,EAAE,CAChI,CACF,CAAC;YACJ,CAAC;YACD,kFAAkF;YAClF,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpG,CAAC;QACD,6EAA6E;QAC7E,iFAAiF;QACjF,0DAA0D;QAC1D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,6CAA6C;IACtC,GAAG,CAAC,MAAmB;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,gDAAgD;IACzC,MAAM,CAAC,MAAmB;QAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC,CAAC;QAC5C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,+CAA+C;IACxC,KAAK,CACV,MAAoB,EACpB,IAAY,EACZ,YAAqB;;QAErB,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC1C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,EAAE,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YACtC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,eAAI,EACF,mCAAmC,MAAM,CAAC,MAAM,mCAAmC,IAAI,CAAC,UAAU,EAAE,CACrG,CACF,CAAC;QACJ,CAAC;QACD,MAAM,cAAc,GAAW,2BAA2B,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACxC,MAAM,CAAC,IAAI,CAAC;oBACV,GAAG,EAAE,IAAA,qBAAa,EAAC,MAAM,CAAC,MAAM,CAAC;oBACjC,GAAG,kBACD,MAAM,EAAE,MAAM,CAAC,MAAM,EACrB,KAAK,EAAE,2BAA2B,CAAC,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,IAChF,QAAQ,CAAC,QAAQ,CACrB;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,4EAA4E;QAC5E,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAsB,EAAE,CAAC;QACnC,8EAA8E;QAC9E,6EAA6E;QAC7E,4EAA4E;QAC5E,wDAAwD;QACxD,MAAM,SAAS,GAAwB,IAAI,GAAG,EAAkB,CAAC;QACjE,KAAK,MAAM,SAAS,IAAI,MAAM,EAAE,CAAC;YAC/B,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;gBACxB,MAAM;YACR,CAAC;YACD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC/B,MAAM,IAAI,GAAW,MAAA,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,mCAAI,CAAC,CAAC;gBACvD,IAAI,IAAI,IAAI,YAAY,EAAE,CAAC;oBACzB,SAAS;gBACX,CAAC;gBACD,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,IAAA,kBAAO,EAAC,IAAI,CAAC,CAAC,CAAC;IACxC,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,iDAAiD;IAC1C,KAAK,CAAC,OAAO,CAClB,MAA2B,EAC3B,KAAuB,EACvB,OAA+B;;QAE/B,MAAM,OAAO,GAAY,CAAC,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,aAAa,mCAAI,MAAM,CAAC,KAAK,MAAM,CAAC;QACvE,MAAM,MAAM,GAAiC,MAAM,IAAA,2BAAU,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACnF,IAAI,MAAM,CAAC,SAAS,EAAE,EAAE,CAAC;YACvB,yEAAyE;YACzE,2EAA2E;YAC3E,uEAAuE;YACvE,0EAA0E;YAC1E,0EAA0E;YAC1E,oCAAoC;YACpC,OAAO,IAAA,yBAAc,EAAC,mDAAmD,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,0EAA0E;QAC1E,yEAAyE;QACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,MAAM,OAAO,GAAsB,IAAI,GAAG,EAAgB,CAAC;QAC3D,MAAM,SAAS,GAAsB,IAAI,GAAG,EAAgB,CAAC;QAC7D,MAAM,QAAQ,GAAsB,IAAI,GAAG,EAAgB,CAAC;QAC5D,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,6EAA6E;QAC7E,4EAA4E;QAC5E,8EAA8E;QAC9E,UAAU;QACV,MAAM,MAAM,GAAG,GAAiC,EAAE,CAAC,CAAC;YAClD,OAAO;YACP,SAAS;YACT,QAAQ;YACR,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ;YAC/B,OAAO;SACR,CAAC,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAS,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC/C,qEAAqE;YACrE,0DAA0D;YAC1D,MAAM,QAAQ,GAA6C,MAAM,IAAA,2BAAU,EAAC,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YACxG,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAW,sCAAsC,IAAA,qBAAa,EAAC,MAAM,CAAC,MAAM,CAAC,aACtF,QAAQ,CAAC,OACX,EAAE,CAAC;gBACH,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,IAAI,CAAC,MAAM,EAAE,CAAC;oBACd,OAAO,IAAA,yBAAc,EAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;gBACzC,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC/C,SAAS;YACX,CAAC;YACD,kEAAkE;YAClE,0EAA0E;YAC1E,4DAA4D;YAC5D,MAAM,KAAK,GAAmB,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;YACrF,IAAI,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;gBACtB,MAAM,KAAK,GAAW,2BAA2B,KAAK,CAAC,OAAO,EAAE,CAAC;gBACjE,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,IAAI,CAAC,MAAM,EAAE,CAAC;oBACd,OAAO,IAAA,yBAAc,EAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;gBACzC,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC/C,SAAS;YACX,CAAC;YACD,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;gBACtB,IAAA,sBAAK,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;gBACtB,SAAS;YACX,CAAC;YACD,yEAAyE;YACzE,4DAA4D;YAC5D,IAAA,sBAAK,EAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACrB,IAAA,sBAAK,EAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,IAAA,4BAAiB,EAAC,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,4DAA4D;IACpD,MAAM;QACZ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,qDAAqD;IAC7C,MAAM,CAAC,UAAU,CAAC,MAAoB;QAC5C,IAAI,GAAG,GAAW,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAW,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/C,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,OAAO,CAAC,KAAmB,EAAE,cAAsB,EAAE,MAAoB;QACtF,MAAM,eAAe,GAAW,2BAA2B,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC/E,IAAI,cAAc,KAAK,CAAC,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;YAClD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,GAAG,GAAW,CAAC,CAAC;QACpB,KAAK,IAAI,CAAC,GAAW,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QACD,OAAO,GAAG,GAAG,CAAC,cAAc,GAAG,eAAe,CAAC,CAAC;IAClD,CAAC;CACF;AArRD,kEAqRC","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<Result<IVectorRebuildReport>>;
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 three ways a record can be absent from the
281
- * index, and that distinction is the entire point: **`declined` was intentional,
282
- * `skipped` was a fault, and neither is the same as "never attempted"**. A caller
283
- * deriving coverage from a count alone cannot tell an embedder outage from a
284
- * deliberate policy, which is precisely the confusion this type exists to end.
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
- /** Records embedded and added to the index. */
289
- readonly indexed: number;
290
- /** Records the embedder deliberately declined (resolved `undefined`). */
291
- readonly declined: number;
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 and no report is returned at all.
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
- /** List every record in the vault, each paired with its scoped address. */
376
- list(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
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