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

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 (182) 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/fragmentSemanticRetriever.js +94 -5
  6. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  8. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  10. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  11. package/dist/packlets/retrieve/retriever.js +91 -10
  12. package/dist/packlets/retrieve/retriever.js.map +1 -1
  13. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  14. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  16. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  18. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  19. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  20. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  21. package/dist/packlets/store/coverage.js +6 -0
  22. package/dist/packlets/store/coverage.js.map +1 -0
  23. package/dist/packlets/store/fileTreeMemoryStore.js +234 -103
  24. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  25. package/dist/packlets/store/index.js +4 -0
  26. package/dist/packlets/store/index.js.map +1 -1
  27. package/dist/packlets/store/listSelection.js +36 -0
  28. package/dist/packlets/store/listSelection.js.map +1 -0
  29. package/dist/packlets/store/memoryStore.js +6 -0
  30. package/dist/packlets/store/memoryStore.js.map +1 -0
  31. package/dist/packlets/store/reconcile.js +6 -0
  32. package/dist/packlets/store/reconcile.js.map +1 -0
  33. package/dist/packlets/store/storeCoverage.js +102 -0
  34. package/dist/packlets/store/storeCoverage.js.map +1 -0
  35. package/dist/packlets/store/storeIdentity.js +62 -0
  36. package/dist/packlets/store/storeIdentity.js.map +1 -0
  37. package/dist/packlets/store/storeReconcile.js +122 -0
  38. package/dist/packlets/store/storeReconcile.js.map +1 -0
  39. package/dist/packlets/store/vectorMaintenance.js +116 -8
  40. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  41. package/dist/packlets/store/vectorRecordSource.js +44 -0
  42. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  43. package/dist/packlets/tools/memoryTools.js +25 -2
  44. package/dist/packlets/tools/memoryTools.js.map +1 -1
  45. package/dist/packlets/types/envelope.js +25 -0
  46. package/dist/packlets/types/envelope.js.map +1 -1
  47. package/dist/packlets/types/identityResolver.js +6 -0
  48. package/dist/packlets/types/identityResolver.js.map +1 -0
  49. package/dist/packlets/types/index.js +2 -0
  50. package/dist/packlets/types/index.js.map +1 -1
  51. package/dist/packlets/types/recordResolver.js +6 -0
  52. package/dist/packlets/types/recordResolver.js.map +1 -0
  53. package/dist/packlets/types/temporal.js.map +1 -1
  54. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  55. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  56. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +109 -14
  57. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  58. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  59. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  60. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  61. package/dist/ts-agent-memory.d.ts +1226 -114
  62. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  63. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  64. package/lib/packlets/index/memoryIndex.js +23 -16
  65. package/lib/packlets/index/memoryIndex.js.map +1 -1
  66. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  67. package/lib/packlets/ingest/orchestrator.js +13 -1
  68. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  69. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
  70. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  71. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  72. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  73. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  74. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  75. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  76. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  77. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  78. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  79. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  80. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  81. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  82. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  83. package/lib/packlets/retrieve/retriever.js +94 -9
  84. package/lib/packlets/retrieve/retriever.js.map +1 -1
  85. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  86. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  87. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  88. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  89. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  90. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  91. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  92. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  93. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  94. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  95. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  96. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  97. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  98. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  99. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  100. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  101. package/lib/packlets/store/coverage.d.ts +102 -0
  102. package/lib/packlets/store/coverage.d.ts.map +1 -0
  103. package/lib/packlets/store/coverage.js +7 -0
  104. package/lib/packlets/store/coverage.js.map +1 -0
  105. package/lib/packlets/store/fileTreeMemoryStore.d.ts +56 -167
  106. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  107. package/lib/packlets/store/fileTreeMemoryStore.js +234 -103
  108. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  109. package/lib/packlets/store/index.d.ts +4 -0
  110. package/lib/packlets/store/index.d.ts.map +1 -1
  111. package/lib/packlets/store/index.js +4 -0
  112. package/lib/packlets/store/index.js.map +1 -1
  113. package/lib/packlets/store/listSelection.d.ts +101 -0
  114. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  115. package/lib/packlets/store/listSelection.js +40 -0
  116. package/lib/packlets/store/listSelection.js.map +1 -0
  117. package/lib/packlets/store/memoryStore.d.ts +237 -0
  118. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  119. package/lib/packlets/store/memoryStore.js +7 -0
  120. package/lib/packlets/store/memoryStore.js.map +1 -0
  121. package/lib/packlets/store/reconcile.d.ts +82 -0
  122. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  123. package/lib/packlets/store/reconcile.js +7 -0
  124. package/lib/packlets/store/reconcile.js.map +1 -0
  125. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  126. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  127. package/lib/packlets/store/storeCoverage.js +105 -0
  128. package/lib/packlets/store/storeCoverage.js.map +1 -0
  129. package/lib/packlets/store/storeIdentity.d.ts +38 -0
  130. package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
  131. package/lib/packlets/store/storeIdentity.js +67 -0
  132. package/lib/packlets/store/storeIdentity.js.map +1 -0
  133. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  134. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  135. package/lib/packlets/store/storeReconcile.js +125 -0
  136. package/lib/packlets/store/storeReconcile.js.map +1 -0
  137. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  138. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  139. package/lib/packlets/store/vectorMaintenance.js +117 -8
  140. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  141. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  142. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  143. package/lib/packlets/store/vectorRecordSource.js +47 -0
  144. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  145. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  146. package/lib/packlets/tools/memoryTools.js +25 -2
  147. package/lib/packlets/tools/memoryTools.js.map +1 -1
  148. package/lib/packlets/types/envelope.d.ts +24 -2
  149. package/lib/packlets/types/envelope.d.ts.map +1 -1
  150. package/lib/packlets/types/envelope.js +26 -0
  151. package/lib/packlets/types/envelope.js.map +1 -1
  152. package/lib/packlets/types/identityResolver.d.ts +42 -0
  153. package/lib/packlets/types/identityResolver.d.ts.map +1 -0
  154. package/lib/packlets/types/identityResolver.js +7 -0
  155. package/lib/packlets/types/identityResolver.js.map +1 -0
  156. package/lib/packlets/types/index.d.ts +2 -0
  157. package/lib/packlets/types/index.d.ts.map +1 -1
  158. package/lib/packlets/types/index.js +2 -0
  159. package/lib/packlets/types/index.js.map +1 -1
  160. package/lib/packlets/types/recordResolver.d.ts +39 -0
  161. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  162. package/lib/packlets/types/recordResolver.js +7 -0
  163. package/lib/packlets/types/recordResolver.js.map +1 -0
  164. package/lib/packlets/types/temporal.d.ts +26 -6
  165. package/lib/packlets/types/temporal.d.ts.map +1 -1
  166. package/lib/packlets/types/temporal.js.map +1 -1
  167. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  168. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  169. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  170. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  171. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +18 -4
  172. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  173. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +108 -13
  174. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  175. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  176. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  177. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  178. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  179. package/lib/packlets/vector/vectorIndex.d.ts +326 -20
  180. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  181. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  182. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAMxE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CActF;AAkBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAC9C,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAEpC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAC/C,IAAI,EAAE,MAAM,GACX,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,CAEpC"}
1
+ {"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAMlE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAchF;AAkBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAE1G;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,gBAAgB,EAC1D,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAC1B,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,SAAS,CAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAYH,4CAEC;AAQD,4CAMC;AASD,4CAcC;AAwBD,oDAIC;AAQD,8CAKC;AAxFD;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,MAA8B;IAC7D,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,MAA8B;IAC7D,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,MAA8B,EAAE,IAAY;;IAC3E,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAW,MAAA,QAAQ,CAAC,QAAQ,mCAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnE,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAA8B,QAAQ,CAAC,UAAU,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,UAAiD;IACnE,IAAI,IAAwC,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrE,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAClC,QAA+C;IAE/C,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,QAA+C,EAC/C,IAAY;IAEZ,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMemoryRecord } from './envelope';\n\n/**\n * Whether a record participates in the versioned (temporal) layout. A temporal\n * record always carries a {@link ITemporalBlock | temporal} block (the store\n * stamps `valid_at` on every versioned write); an atemporal record never does,\n * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`\n * divergence (MTM is flat yet has `entityId !== id`).\n * @public\n */\nexport function isTemporalRecord(record: IMemoryRecord<unknown>): boolean {\n return record.envelope.temporal !== undefined;\n}\n\n/**\n * Whether a temporal record is a *current* version — its `temporal.invalid_at`\n * is `null` or absent (the still-valid sentinel). A non-temporal record is never\n * current in this sense (returns `false`).\n * @public\n */\nexport function isVersionCurrent(record: IMemoryRecord<unknown>): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n return temporal.invalid_at === null || temporal.invalid_at === undefined;\n}\n\n/**\n * Whether a temporal record's validity interval contains `asOf` (epoch ms):\n * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).\n * The version's `valid_at` defaults to its `created` when absent; a non-temporal\n * record is never \"valid at\" a point (returns `false`).\n * @public\n */\nexport function isVersionValidAt(record: IMemoryRecord<unknown>, asOf: number): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n const start: number = temporal.valid_at ?? record.envelope.created;\n if (start > asOf) {\n return false;\n }\n const end: number | null | undefined = temporal.invalid_at;\n if (end === null || end === undefined) {\n return true;\n }\n return asOf < end;\n}\n\n/**\n * The version with the highest `seq` among `candidates` (undefined when empty).\n * `seq` is the store's monotonic write counter, so highest `seq` is the newest\n * version. Shared tiebreak for {@link selectCurrentVersion} /\n * {@link selectVersionAsOf}.\n */\nfunction highestSeq(candidates: ReadonlyArray<IMemoryRecord<unknown>>): IMemoryRecord<unknown> | undefined {\n let best: IMemoryRecord<unknown> | undefined;\n for (const candidate of candidates) {\n if (best === undefined || candidate.envelope.seq > best.envelope.seq) {\n best = candidate;\n }\n }\n return best;\n}\n\n/**\n * Select the current version from a set of an entity's versions: the newest\n * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the\n * entity has no current version (fully invalidated / soft-deleted, or empty).\n * @public\n */\nexport function selectCurrentVersion(\n versions: ReadonlyArray<IMemoryRecord<unknown>>\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter(isVersionCurrent));\n}\n\n/**\n * Select the version of an entity valid at `asOf` (epoch ms): the newest\n * (highest `seq`) version whose validity interval contains `asOf`. `undefined`\n * when no version was valid at that instant.\n * @public\n */\nexport function selectVersionAsOf(\n versions: ReadonlyArray<IMemoryRecord<unknown>>,\n asOf: number\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));\n}\n"]}
1
+ {"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAiCH,4CAEC;AAQD,4CAMC;AASD,4CAcC;AAwBD,oDAEC;AAQD,8CAKC;AAtFD;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,MAAwB;IACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,MAAwB;IACvD,MAAM,QAAQ,GAAgC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,MAAwB,EAAE,IAAY;;IACrE,MAAM,QAAQ,GAAgC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAW,MAAA,QAAQ,CAAC,QAAQ,mCAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnE,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAA8B,QAAQ,CAAC,UAAU,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAA6B,UAA4B;IAC1E,IAAI,IAAmB,CAAC;IACxB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrE,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAA6B,QAA0B;IACzF,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,QAA0B,EAC1B,IAAY;IAEZ,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMemoryEnvelope } from './envelope';\n\n/**\n * The minimum a temporal helper needs: something carrying an envelope.\n *\n * @remarks\n * Every predicate and selector in this module reads `envelope.temporal`,\n * `envelope.created` and `envelope.seq` and **nothing else** — no body has ever\n * been consulted. Taking the structural shape rather than `IMemoryRecord` lets\n * the same helpers serve both an `IMemoryRecord<unknown>` (which satisfies it)\n * and an `IIndexedMemoryEntry` from the index's projected read surface, so the\n * temporal `asOf` projection can run over envelopes and materialize only the\n * versions that survive it.\n *\n * The selectors are generic in this shape so they return exactly what they were\n * given rather than widening to the constraint.\n * @public\n */\nexport interface IEnvelopeCarrier {\n /** The envelope the temporal predicates read. */\n readonly envelope: IMemoryEnvelope;\n}\n\n/**\n * Whether a record participates in the versioned (temporal) layout. A temporal\n * record always carries a {@link ITemporalBlock | temporal} block (the store\n * stamps `valid_at` on every versioned write); an atemporal record never does,\n * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`\n * divergence (MTM is flat yet has `entityId !== id`).\n * @public\n */\nexport function isTemporalRecord(record: IEnvelopeCarrier): boolean {\n return record.envelope.temporal !== undefined;\n}\n\n/**\n * Whether a temporal record is a *current* version — its `temporal.invalid_at`\n * is `null` or absent (the still-valid sentinel). A non-temporal record is never\n * current in this sense (returns `false`).\n * @public\n */\nexport function isVersionCurrent(record: IEnvelopeCarrier): boolean {\n const temporal: IMemoryEnvelope['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n return temporal.invalid_at === null || temporal.invalid_at === undefined;\n}\n\n/**\n * Whether a temporal record's validity interval contains `asOf` (epoch ms):\n * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).\n * The version's `valid_at` defaults to its `created` when absent; a non-temporal\n * record is never \"valid at\" a point (returns `false`).\n * @public\n */\nexport function isVersionValidAt(record: IEnvelopeCarrier, asOf: number): boolean {\n const temporal: IMemoryEnvelope['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n const start: number = temporal.valid_at ?? record.envelope.created;\n if (start > asOf) {\n return false;\n }\n const end: number | null | undefined = temporal.invalid_at;\n if (end === null || end === undefined) {\n return true;\n }\n return asOf < end;\n}\n\n/**\n * The version with the highest `seq` among `candidates` (undefined when empty).\n * `seq` is the store's monotonic write counter, so highest `seq` is the newest\n * version. Shared tiebreak for {@link selectCurrentVersion} /\n * {@link selectVersionAsOf}.\n */\nfunction highestSeq<T extends IEnvelopeCarrier>(candidates: ReadonlyArray<T>): T | undefined {\n let best: T | undefined;\n for (const candidate of candidates) {\n if (best === undefined || candidate.envelope.seq > best.envelope.seq) {\n best = candidate;\n }\n }\n return best;\n}\n\n/**\n * Select the current version from a set of an entity's versions: the newest\n * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the\n * entity has no current version (fully invalidated / soft-deleted, or empty).\n * @public\n */\nexport function selectCurrentVersion<T extends IEnvelopeCarrier>(versions: ReadonlyArray<T>): T | undefined {\n return highestSeq(versions.filter(isVersionCurrent));\n}\n\n/**\n * Select the version of an entity valid at `asOf` (epoch ms): the newest\n * (highest `seq`) version whose validity interval contains `asOf`. `undefined`\n * when no version was valid at that instant.\n * @public\n */\nexport function selectVersionAsOf<T extends IEnvelopeCarrier>(\n versions: ReadonlyArray<T>,\n asOf: number\n): T | undefined {\n return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Result } from '@fgv/ts-utils';
1
+ import { DetailedResult, Result } from '@fgv/ts-utils';
2
2
  import { IEdgeTarget } from '../types';
3
3
  import { IMemoryRecordSource, IVectorIndex, IVectorQueryHit, IVectorRebuildOptions, IVectorRebuildReport, MemoryEmbedder } from './vectorIndex';
4
4
  /**
@@ -40,6 +40,8 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
40
40
  static create(): Result<InMemoryCosineIndex>;
41
41
  /** {@inheritDoc IVectorIndex.add} */
42
42
  add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>>;
43
+ /** {@inheritDoc IVectorIndex.has} */
44
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
43
45
  /** {@inheritDoc IVectorIndex.remove} */
44
46
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
45
47
  /** {@inheritDoc IVectorIndex.query} */
@@ -72,6 +74,11 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
72
74
  * A {@link MemoryEmbedder} decline is not a failure under either mode: it is
73
75
  * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.
74
76
  *
77
+ * **A `'fail'` failure carries the partial report on its `detail`** — the
78
+ * rollback still runs, so that report describes the aborted attempt rather than
79
+ * the (now empty) index. The one failure with no detail is a `list` failure,
80
+ * which disturbs nothing and has nothing to describe.
81
+ *
75
82
  * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`
76
83
  * that throws or rejects becomes a `Failure` on the path above rather than an
77
84
  * exception escaping mid-rebuild — which would bypass the rollback entirely.
@@ -80,7 +87,7 @@ export declare class InMemoryCosineIndex implements IVectorIndex {
80
87
  * @param embed - The embedder applied to each record.
81
88
  * @param options - Rebuild options; omit for the historical `'fail'` behavior.
82
89
  */
83
- rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<Result<IVectorRebuildReport>>;
90
+ rebuild(source: IMemoryRecordSource, embed: MemoryEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>>;
84
91
  /** Empty the index and forget the established dimension. */
85
92
  private _reset;
86
93
  /** The Euclidean magnitude (L2 norm) of a vector. */
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAqC,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,mBAAmB,EAGnB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACf,MAAM,eAAe,CAAC;AAkBvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAsB9E,wCAAwC;IACjC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyBjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACU,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAwDxC,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAuB,MAAM,UAAU,CAAC;AAC5D,OAAO,EAEL,mBAAmB,EAEnB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACf,MAAM,eAAe,CAAC;AASvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,4CAA4C;IAC5C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,mBAAmB,CAAC;IAInD,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAsB9E,qCAAqC;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,wCAAwC;IACjC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,uCAAuC;IAChC,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAyBjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACU,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,oBAAoB,EAAE,oBAAoB,CAAC,CAAC;IA4EtE,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
@@ -7,15 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.InMemoryCosineIndex = void 0;
8
8
  const ts_utils_1 = require("@fgv/ts-utils");
9
9
  const types_1 = require("../types");
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
- async function invokeHook(hook) {
17
- return (await (0, ts_utils_1.captureAsyncResult)(hook)).onSuccess((inner) => inner);
18
- }
10
+ const rebuildHelpers_1 = require("./rebuildHelpers");
19
11
  /**
20
12
  * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one
21
13
  * `Float32Array` per record and answers a query by computing cosine similarity
@@ -71,6 +63,10 @@ class InMemoryCosineIndex {
71
63
  // that key IS the entry reference.
72
64
  return Promise.resolve((0, ts_utils_1.succeed)(key));
73
65
  }
66
+ /** {@inheritDoc IVectorIndex.has} */
67
+ has(target) {
68
+ return Promise.resolve((0, ts_utils_1.succeed)(this._vectors.has((0, types_1.edgeTargetKey)(target))));
69
+ }
74
70
  /** {@inheritDoc IVectorIndex.remove} */
75
71
  remove(target) {
76
72
  this._vectors.delete((0, types_1.edgeTargetKey)(target));
@@ -125,6 +121,11 @@ class InMemoryCosineIndex {
125
121
  * A {@link MemoryEmbedder} decline is not a failure under either mode: it is
126
122
  * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.
127
123
  *
124
+ * **A `'fail'` failure carries the partial report on its `detail`** — the
125
+ * rollback still runs, so that report describes the aborted attempt rather than
126
+ * the (now empty) index. The one failure with no detail is a `list` failure,
127
+ * which disturbs nothing and has nothing to describe.
128
+ *
128
129
  * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`
129
130
  * that throws or rejects becomes a `Failure` on the path above rather than an
130
131
  * exception escaping mid-rebuild — which would bypass the rollback entirely.
@@ -136,7 +137,7 @@ class InMemoryCosineIndex {
136
137
  async rebuild(source, embed, options) {
137
138
  var _a;
138
139
  const lenient = ((_a = options === null || options === void 0 ? void 0 : options.onRecordError) !== null && _a !== void 0 ? _a : 'fail') === 'skip';
139
- const listed = await invokeHook(() => source.list());
140
+ const listed = await (0, rebuildHelpers_1.invokeHook)(() => source.list());
140
141
  if (listed.isFailure()) {
141
142
  // Deliberately BEFORE the reset. This used to reset first, on the reasoning
142
143
  // that no stale vectors should survive a failed rebuild — but a failed list
@@ -144,35 +145,50 @@ class InMemoryCosineIndex {
144
145
  // re-embedded yet, so there is no half-rebuilt state to guard against.
145
146
  // Leaving the prior contents intact is the more correct answer, and on the
146
147
  // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.
147
- return (0, ts_utils_1.fail)(`vector index rebuild: failed to list records: ${listed.message}`);
148
+ //
149
+ // No detail, for the same reason: an all-zero report would describe an index
150
+ // this call never touched.
151
+ return (0, ts_utils_1.failWithDetail)(`vector index rebuild: failed to list records: ${listed.message}`);
148
152
  }
149
153
  // From here a rebuild is genuinely starting, so clear. A mid-loop failure
150
154
  // under `'fail'` still resets, which is what keeps that contract honest.
151
155
  this._reset();
152
- let declined = 0;
156
+ const indexed = new Map();
157
+ const declined = new Map();
153
158
  const skipped = [];
154
- for (const scoped of listed.value) {
159
+ // Only the source knows what it filtered, so an absent `excluded` propagates
160
+ // as absent rather than becoming an empty map — "cannot say" and "excluded
161
+ // nothing" are different answers.
162
+ const report = () => ({
163
+ indexed,
164
+ declined,
165
+ excluded: listed.value.excluded,
166
+ skipped
167
+ });
168
+ for (const scoped of listed.value.records) {
169
+ const kind = scoped.record.envelope.kind;
155
170
  // Both hooks are consumer-supplied, so a throw or rejection is captured
156
171
  // into a `Failure` rather than escaping as an exception — otherwise a
157
172
  // badly-behaved embedder would reject out of `rebuild` mid-loop and leave
158
173
  // the index half-populated, which is precisely what the rollback below
159
174
  // exists to prevent.
160
- const embedded = await invokeHook(() => embed(scoped.record));
175
+ const embedded = await (0, rebuildHelpers_1.invokeHook)(() => embed(scoped.record));
161
176
  if (embedded.isFailure()) {
162
177
  const error = `vector index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`;
163
178
  // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and
164
179
  // abort, so a caller that retries a query sees a clean empty index rather
165
- // than a partially-rebuilt one it cannot reason about.
180
+ // than a partially-rebuilt one it cannot reason about. What is new is only
181
+ // that the failure also carries what the attempt had established.
166
182
  if (!lenient) {
167
183
  this._reset();
168
- return (0, ts_utils_1.fail)(error);
184
+ return (0, ts_utils_1.failWithDetail)(error, report());
169
185
  }
170
186
  skipped.push({ target: scoped.target, error });
171
187
  continue;
172
188
  }
173
189
  // A decline is not an error under either mode — it is counted, never skipped.
174
190
  if (embedded.value === undefined) {
175
- declined++;
191
+ (0, rebuildHelpers_1.tally)(declined, kind);
176
192
  continue;
177
193
  }
178
194
  const added = await this.add(scoped.target, embedded.value);
@@ -180,12 +196,17 @@ class InMemoryCosineIndex {
180
196
  const error = `vector index rebuild: ${added.message}`;
181
197
  if (!lenient) {
182
198
  this._reset();
183
- return (0, ts_utils_1.fail)(error);
199
+ return (0, ts_utils_1.failWithDetail)(error, report());
184
200
  }
185
201
  skipped.push({ target: scoped.target, error });
202
+ continue;
186
203
  }
204
+ // Tallied per successful add rather than read back off `size` at the end:
205
+ // `size` counts distinct addresses, and this count must line up with its
206
+ // per-record siblings so the three still sum to the records seen.
207
+ (0, rebuildHelpers_1.tally)(indexed, kind);
187
208
  }
188
- return (0, ts_utils_1.succeed)({ indexed: this._vectors.size, declined, skipped });
209
+ return (0, ts_utils_1.succeedWithDetail)(report());
189
210
  }
190
211
  /** Empty the index and forget the established dimension. */
191
212
  _reset() {
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureAsyncResult, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, edgeTargetKey } from '../types';\nimport {\n IMemoryRecordSource,\n IScopedMemoryRecord,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n MemoryEmbedder\n} from './vectorIndex';\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 */\nasync function invokeHook<T>(hook: () => Promise<Result<T>>): Promise<Result<T>> {\n return (await captureAsyncResult(hook)).onSuccess((inner) => inner);\n}\n\n/** One stored embedding: the scope-qualified address plus its vector. */\ninterface IStoredVector {\n readonly target: IEdgeTarget;\n readonly vector: Float32Array;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n /**\n * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the\n * record's scope-qualified address, so two records that share a filename stem\n * across scopes occupy distinct entries and never overwrite each other.\n */\n private readonly _vectors: Map<string, IStoredVector>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<string, IStoredVector>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(key, { target, vector: Float32Array.from(vector) });\n // The in-memory index keys entries by the canonical scoped-target string, so\n // that key IS the entry reference.\n return Promise.resolve(succeed(key));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._vectors.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const stored of this._vectors.values()) {\n hits.push({\n target: stored.target,\n score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)\n });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns an\n * {@link IVectorRebuildReport} describing what was indexed, declined and skipped.\n *\n * @remarks\n * **A failure to LIST is always fatal**, under either mode — and **leaves the\n * existing index untouched**: an unreadable source says nothing about which\n * records exist, so there is neither an honest partial to report nor any reason\n * to discard what is already held.\n *\n * Per-record embed/add failures are governed by\n * {@link IVectorRebuildOptions.onRecordError}, which defaults to `'fail'` —\n * **the historical all-or-nothing contract, unchanged**: the index is rolled back\n * to empty rather than left partially rebuilt, so a caller that retries a query\n * sees a clean empty index it can reason about.\n *\n * `'skip'` opts into the lenient shape the store's own open already uses: the\n * rebuild continues and every casualty is returned structurally on\n * {@link IVectorRebuildReport.skipped}. **It reports more, it does not report\n * less** — the point is to stop one bad record emptying an entire index, not to\n * make failures quieter. A caller that ignores `skipped` under `'skip'` has\n * chosen to, rather than been given no way to know.\n *\n * A {@link MemoryEmbedder} decline is not a failure under either mode: it is\n * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.\n *\n * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`\n * that throws or rejects becomes a `Failure` on the path above rather than an\n * exception escaping mid-rebuild — which would bypass the rollback entirely.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The embedder applied to each record.\n * @param options - Rebuild options; omit for the historical `'fail'` behavior.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<Result<IVectorRebuildReport>> {\n const lenient: boolean = (options?.onRecordError ?? 'fail') === 'skip';\n const listed: Result<ReadonlyArray<IScopedMemoryRecord>> = await invokeHook(() => source.list());\n if (listed.isFailure()) {\n // Deliberately BEFORE the reset. This used to reset first, on the reasoning\n // that no stale vectors should survive a failed rebuild — but a failed list\n // is no evidence about the vectors already held, and nothing has been\n // re-embedded yet, so there is no half-rebuilt state to guard against.\n // Leaving the prior contents intact is the more correct answer, and on the\n // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.\n return fail(`vector 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 let declined: number = 0;\n const skipped: ISkippedVectorRecord[] = [];\n for (const scoped of listed.value) {\n // Both hooks are consumer-supplied, so a throw or rejection is captured\n // into a `Failure` rather than escaping as an exception — otherwise a\n // badly-behaved embedder would reject out of `rebuild` mid-loop and leave\n // the index half-populated, which is precisely what the rollback below\n // exists to prevent.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and\n // abort, so a caller that retries a query sees a clean empty index rather\n // than a partially-rebuilt one it cannot reason about.\n if (!lenient) {\n this._reset();\n return fail(error);\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // A decline is not an error under either mode — it is counted, never skipped.\n if (embedded.value === undefined) {\n declined++;\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector index rebuild: ${added.message}`;\n if (!lenient) {\n this._reset();\n return fail(error);\n }\n skipped.push({ target: scoped.target, error });\n }\n }\n return succeed({ indexed: this._vectors.size, declined, skipped });\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.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 vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._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":"inMemoryCosineIndex.js","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryCosineIndex.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 IMemoryRecordListing,\n IMemoryRecordSource,\n ISkippedVectorRecord,\n IVectorIndex,\n IVectorQueryHit,\n IVectorRebuildOptions,\n IVectorRebuildReport,\n MemoryEmbedder\n} from './vectorIndex';\nimport { invokeHook, tally } from './rebuildHelpers';\n\n/** One stored embedding: the scope-qualified address plus its vector. */\ninterface IStoredVector {\n readonly target: IEdgeTarget;\n readonly vector: Float32Array;\n}\n\n/**\n * The brute-force, in-memory cosine {@link IVectorIndex}. Stores one\n * `Float32Array` per record and answers a query by computing cosine similarity\n * against every stored vector, returning the top-k by descending score.\n *\n * @remarks\n * This is the **complete** vector implementation for the fgv regime — large-N is\n * explicitly out of scope (the seam stays open for a consumer to swap an external\n * ANN backend once N grows beyond \"thousands of records\"). No external dependency\n * and no ANN structure: a linear scan over a few thousand vectors is well within\n * an interactive budget.\n *\n * The index has a single dimension established by the first vector added; every\n * subsequent `add` and every `query` vector must match that dimension or fail\n * loudly — a mismatched dimension is an embedder-wiring bug, never a silent\n * zero-similarity result. {@link InMemoryCosineIndex.rebuild | rebuild} clears\n * the index, so a re-embed with a different model (hence dimension) is supported.\n *\n * Persistence (a JSON sidecar) is deliberately out of scope for this layer — the\n * index is in-memory and rebuilt from the store via `rebuild`; a sidecar is a\n * future nicety.\n * @public\n */\nexport class InMemoryCosineIndex implements IVectorIndex {\n /**\n * Stored embeddings keyed by the canonical {@link edgeTargetKey} of the\n * record's scope-qualified address, so two records that share a filename stem\n * across scopes occupy distinct entries and never overwrite each other.\n */\n private readonly _vectors: Map<string, IStoredVector>;\n /** The dimension of every stored vector; `undefined` until the first `add`. */\n private _dimension: number | undefined;\n\n private constructor() {\n this._vectors = new Map<string, IStoredVector>();\n this._dimension = undefined;\n }\n\n /** The number of vectors currently held. */\n public get size(): number {\n return this._vectors.size;\n }\n\n /** Family-convention factory. */\n public static create(): Result<InMemoryCosineIndex> {\n return succeed(new InMemoryCosineIndex());\n }\n\n /** {@inheritDoc IVectorIndex.add} */\n public add(target: IEdgeTarget, vector: Float32Array): Promise<Result<string>> {\n const key: string = edgeTargetKey(target);\n if (vector.length === 0) {\n return Promise.resolve(fail(`vector index: cannot add '${key}': empty vector`));\n }\n if (this._dimension === undefined) {\n this._dimension = vector.length;\n } else if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: cannot add '${key}': dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n // Defensive copy: the caller may reuse or mutate the buffer after `add`, and\n // the index must keep serving the embedding it was given.\n this._vectors.set(key, { target, vector: Float32Array.from(vector) });\n // The in-memory index keys entries by the canonical scoped-target string, so\n // that key IS the entry reference.\n return Promise.resolve(succeed(key));\n }\n\n /** {@inheritDoc IVectorIndex.has} */\n public has(target: IEdgeTarget): Promise<Result<boolean>> {\n return Promise.resolve(succeed(this._vectors.has(edgeTargetKey(target))));\n }\n\n /** {@inheritDoc IVectorIndex.remove} */\n public remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>> {\n this._vectors.delete(edgeTargetKey(target));\n return Promise.resolve(succeed(target));\n }\n\n /** {@inheritDoc IVectorIndex.query} */\n public query(vector: Float32Array, topK: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (topK <= 0 || this._vectors.size === 0) {\n return Promise.resolve(succeed([]));\n }\n if (vector.length !== this._dimension) {\n return Promise.resolve(\n fail(\n `vector index: query dimension ${vector.length} does not match index dimension ${this._dimension}`\n )\n );\n }\n const queryMagnitude: number = InMemoryCosineIndex._magnitude(vector);\n const hits: IVectorQueryHit[] = [];\n for (const stored of this._vectors.values()) {\n hits.push({\n target: stored.target,\n score: InMemoryCosineIndex._cosine(vector, queryMagnitude, stored.vector)\n });\n }\n // Descending by score; a `seq`-free tiebreak is unnecessary here because the\n // caller (SemanticRetriever) re-resolves hits against the record index.\n hits.sort((a, b) => b.score - a.score);\n return Promise.resolve(succeed(hits.length > topK ? hits.slice(0, topK) : hits));\n }\n\n /**\n * Re-embed every record from `source` and rebuild the index from scratch.\n * Clears the current contents (and the established dimension) first, so a\n * re-embed with a different model is supported. Returns an\n * {@link IVectorRebuildReport} describing what was indexed, declined and skipped.\n *\n * @remarks\n * **A failure to LIST is always fatal**, under either mode — and **leaves the\n * existing index untouched**: an unreadable source says nothing about which\n * records exist, so there is neither an honest partial to report nor any reason\n * to discard what is already held.\n *\n * Per-record embed/add failures are governed by\n * {@link IVectorRebuildOptions.onRecordError}, which defaults to `'fail'` —\n * **the historical all-or-nothing contract, unchanged**: the index is rolled back\n * to empty rather than left partially rebuilt, so a caller that retries a query\n * sees a clean empty index it can reason about.\n *\n * `'skip'` opts into the lenient shape the store's own open already uses: the\n * rebuild continues and every casualty is returned structurally on\n * {@link IVectorRebuildReport.skipped}. **It reports more, it does not report\n * less** — the point is to stop one bad record emptying an entire index, not to\n * make failures quieter. A caller that ignores `skipped` under `'skip'` has\n * chosen to, rather than been given no way to know.\n *\n * A {@link MemoryEmbedder} decline is not a failure under either mode: it is\n * counted on {@link IVectorRebuildReport.declined} and never appears in `skipped`.\n *\n * **A `'fail'` failure carries the partial report on its `detail`** — the\n * rollback still runs, so that report describes the aborted attempt rather than\n * the (now empty) index. The one failure with no detail is a `list` failure,\n * which disturbs nothing and has nothing to describe.\n *\n * Both consumer-supplied hooks are capture-wrapped, so a `source` or `embed`\n * that throws or rejects becomes a `Failure` on the path above rather than an\n * exception escaping mid-rebuild — which would bypass the rollback entirely.\n *\n * @param source - The scope-qualified record source to re-embed.\n * @param embed - The embedder applied to each record.\n * @param options - Rebuild options; omit for the historical `'fail'` behavior.\n */\n public async rebuild(\n source: IMemoryRecordSource,\n embed: MemoryEmbedder,\n options?: IVectorRebuildOptions\n ): Promise<DetailedResult<IVectorRebuildReport, IVectorRebuildReport>> {\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. This used to reset first, on the reasoning\n // that no stale vectors should survive a failed rebuild — but a failed list\n // is no evidence about the vectors already held, and nothing has been\n // re-embedded yet, so there is no half-rebuilt state to guard against.\n // Leaving the prior contents intact is the more correct answer, and on the\n // durable sibling (`SqliteVecVectorIndex`) resetting here was data loss.\n //\n // No detail, for the same reason: an all-zero report would describe an index\n // this call never touched.\n return failWithDetail(`vector 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 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 — \"cannot say\" and \"excluded\n // nothing\" are different answers.\n const report = (): IVectorRebuildReport => ({\n indexed,\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 // Both hooks are consumer-supplied, so a throw or rejection is captured\n // into a `Failure` rather than escaping as an exception — otherwise a\n // badly-behaved embedder would reject out of `rebuild` mid-loop and leave\n // the index half-populated, which is precisely what the rollback below\n // exists to prevent.\n const embedded: Result<Float32Array | undefined> = await invokeHook(() => embed(scoped.record));\n if (embedded.isFailure()) {\n const error: string = `vector index rebuild: embedding '${edgeTargetKey(scoped.target)}' failed: ${\n embedded.message\n }`;\n // `'fail'` keeps the historical all-or-nothing contract EXACTLY: reset and\n // abort, so a caller that retries a query sees a clean empty index rather\n // than a partially-rebuilt one it cannot reason about. What is new is only\n // that the failure also carries what the attempt had established.\n if (!lenient) {\n this._reset();\n return failWithDetail(error, report());\n }\n skipped.push({ target: scoped.target, error });\n continue;\n }\n // A decline is not an error under either mode — it is counted, never skipped.\n if (embedded.value === undefined) {\n tally(declined, kind);\n continue;\n }\n const added: Result<string> = await this.add(scoped.target, embedded.value);\n if (added.isFailure()) {\n const error: string = `vector 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 // Tallied per successful add rather than read back off `size` at the end:\n // `size` counts distinct addresses, and this count must line up with its\n // per-record siblings so the three still sum to the records seen.\n tally(indexed, kind);\n }\n return succeedWithDetail(report());\n }\n\n /** Empty the index and forget the established dimension. */\n private _reset(): void {\n this._vectors.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 vector. A zero-magnitude vector on\n * either side yields `0` rather than `NaN` — a degenerate vector is simply\n * maximally dissimilar, not an error.\n */\n private static _cosine(query: Float32Array, queryMagnitude: number, stored: Float32Array): number {\n const storedMagnitude: number = InMemoryCosineIndex._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,6 +1,6 @@
1
- import { Result } from '@fgv/ts-utils';
1
+ import { DetailedResult, Result } from '@fgv/ts-utils';
2
2
  import { IEdgeTarget } from '../types';
3
- import { FragmentEmbedder, IEmbeddedFragment, IFragmentVectorIndex, IMemoryRecordSource, IVectorQueryHit } from './vectorIndex';
3
+ import { FragmentEmbedder, IEmbeddedFragment, IFragmentQueryOptions, IFragmentVectorIndex, IFragmentVectorRebuildReport, IMemoryRecordSource, IVectorQueryHit, IVectorRebuildOptions } from './vectorIndex';
4
4
  /**
5
5
  * The brute-force, in-memory cosine {@link IFragmentVectorIndex} — the
6
6
  * fragment-granular sibling of {@link InMemoryCosineIndex}. Stores many
@@ -45,10 +45,23 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
45
45
  static create(): Result<InMemoryFragmentCosineIndex>;
46
46
  /** {@inheritDoc IFragmentVectorIndex.addFragments} */
47
47
  addFragments(target: IEdgeTarget, fragments: ReadonlyArray<IEmbeddedFragment>): Promise<Result<number>>;
48
+ /** {@inheritDoc IFragmentVectorIndex.has} */
49
+ has(target: IEdgeTarget): Promise<Result<boolean>>;
48
50
  /** {@inheritDoc IFragmentVectorIndex.remove} */
49
51
  remove(target: IEdgeTarget): Promise<Result<IEdgeTarget>>;
50
52
  /** {@inheritDoc IFragmentVectorIndex.query} */
51
- query(vector: Float32Array, topK: number, maxPerRecord?: number): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
53
+ query(vector: Float32Array, topK: number, options?: IFragmentQueryOptions): Promise<Result<ReadonlyArray<IVectorQueryHit>>>;
54
+ /**
55
+ * The records a query is allowed to score, honoring the `scope` / `id` narrowing.
56
+ *
57
+ * @remarks
58
+ * The single-record case is an O(1) map lookup rather than a scan, because the
59
+ * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's
60
+ * per-entity subtree) is a filtered walk — still bounded by the vault, but it
61
+ * scores only the entity's own fragments, which is what makes the caller's `topK`
62
+ * meaningful.
63
+ */
64
+ private _selectRecords;
52
65
  /**
53
66
  * Re-embed every record from `source` and rebuild the fragment index from
54
67
  * scratch. Clears the current contents (and the established dimension) first, so
@@ -68,7 +81,8 @@ export declare class InMemoryFragmentCosineIndex implements IFragmentVectorIndex
68
81
  * @param source - The scope-qualified record source to re-embed.
69
82
  * @param embed - The fragment embedder applied to each record.
70
83
  */
71
- rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder): Promise<Result<number>>;
84
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
85
+ rebuild(source: IMemoryRecordSource, embed: FragmentEmbedder, options?: IVectorRebuildOptions): Promise<DetailedResult<IFragmentVectorRebuildReport, IFragmentVectorRebuildReport>>;
72
86
  /** Empty the index and forget the established dimension. */
73
87
  private _reset;
74
88
  /** The Euclidean magnitude (L2 norm) of a vector. */
@@ -1 +1 @@
1
- {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AACtD,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EAEnB,eAAe,EAChB,MAAM,eAAe,CAAC;AAoCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAkDlD;;;;;;;;;;;;;;;;;;OAkBG;IACU,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAuBnG,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
1
+ {"version":3,"file":"inMemoryFragmentCosineIndex.d.ts","sourceRoot":"","sources":["../../../src/packlets/vector/inMemoryFragmentCosineIndex.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,WAAW,EAAiD,MAAM,UAAU,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAE5B,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAqCvB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,2BAA4B,YAAW,oBAAoB;IACtE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,wFAAwF;IACxF,OAAO,CAAC,UAAU,CAAqB;IAEvC,OAAO;IAKP,8EAA8E;IAC9E,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED,uEAAuE;IACvE,IAAW,aAAa,IAAI,MAAM,CAMjC;IAED,iCAAiC;WACnB,MAAM,IAAI,MAAM,CAAC,2BAA2B,CAAC;IAI3D,sDAAsD;IAC/C,YAAY,CACjB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IA+C1B,6CAA6C;IACtC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAIzD,gDAAgD;IACzC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAKhE,+CAA+C;IACxC,KAAK,CACV,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IAwDlD;;;;;;;;;OASG;IACH,OAAO,CAAE,cAAc;IAyBvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,iDAAiD;IACpC,OAAO,CAClB,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC;IAuEtF,4DAA4D;IAC5D,OAAO,CAAC,MAAM;IAKd,qDAAqD;IACrD,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,OAAO;CAWvB"}
@@ -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,14 +100,19 @@ 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));
105
110
  return Promise.resolve((0, ts_utils_1.succeed)(target));
106
111
  }
107
112
  /** {@inheritDoc IFragmentVectorIndex.query} */
108
- query(vector, topK, maxPerRecord) {
113
+ query(vector, topK, options) {
109
114
  var _a;
115
+ const maxPerRecord = options === null || options === void 0 ? void 0 : options.maxPerRecord;
110
116
  if (topK <= 0 || this._records.size === 0) {
111
117
  return Promise.resolve((0, ts_utils_1.succeed)([]));
112
118
  }
@@ -115,7 +121,12 @@ class InMemoryFragmentCosineIndex {
115
121
  }
116
122
  const queryMagnitude = InMemoryFragmentCosineIndex._magnitude(vector);
117
123
  const scored = [];
118
- for (const record of this._records.values()) {
124
+ // The narrowing is applied HERE — choosing which records are scored at all —
125
+ // rather than by filtering hits afterwards. That is the whole point: a
126
+ // post-filter would leave `topK` applied to the global set, so a scoped search
127
+ // would silently return fewer than `topK` whenever other records outscored the
128
+ // target's fragments.
129
+ for (const record of this._selectRecords(options)) {
119
130
  for (const fragment of record.fragments) {
120
131
  scored.push({
121
132
  key: (0, types_1.edgeTargetKey)(record.target),
@@ -146,6 +157,40 @@ class InMemoryFragmentCosineIndex {
146
157
  }
147
158
  return Promise.resolve((0, ts_utils_1.succeed)(hits));
148
159
  }
160
+ /**
161
+ * The records a query is allowed to score, honoring the `scope` / `id` narrowing.
162
+ *
163
+ * @remarks
164
+ * The single-record case is an O(1) map lookup rather than a scan, because the
165
+ * record map is keyed by `edgeTargetKey`. The scope-only case (a versioned kind's
166
+ * per-entity subtree) is a filtered walk — still bounded by the vault, but it
167
+ * scores only the entity's own fragments, which is what makes the caller's `topK`
168
+ * meaningful.
169
+ */
170
+ *_selectRecords(options) {
171
+ // Narrowed once, deliberately: re-deriving through `options?.` a second time
172
+ // after an early return that already implies `options !== undefined` creates an
173
+ // optional-chain arm that cannot fire, which is a dead branch rather than an
174
+ // untested one.
175
+ if (options === undefined || options.scope === undefined) {
176
+ yield* this._records.values();
177
+ return;
178
+ }
179
+ const scope = options.scope;
180
+ const id = options.id;
181
+ if (id !== undefined) {
182
+ const record = this._records.get((0, types_1.edgeTargetKey)({ scope, id }));
183
+ if (record !== undefined) {
184
+ yield record;
185
+ }
186
+ return;
187
+ }
188
+ for (const record of this._records.values()) {
189
+ if (record.target.scope === scope) {
190
+ yield record;
191
+ }
192
+ }
193
+ }
149
194
  /**
150
195
  * Re-embed every record from `source` and rebuild the fragment index from
151
196
  * scratch. Clears the current contents (and the established dimension) first, so
@@ -165,25 +210,75 @@ class InMemoryFragmentCosineIndex {
165
210
  * @param source - The scope-qualified record source to re-embed.
166
211
  * @param embed - The fragment embedder applied to each record.
167
212
  */
168
- async rebuild(source, embed) {
169
- this._reset();
170
- const listed = await source.list();
213
+ /** {@inheritDoc IFragmentVectorIndex.rebuild} */
214
+ async rebuild(source, embed, options) {
215
+ var _a;
216
+ const lenient = ((_a = options === null || options === void 0 ? void 0 : options.onRecordError) !== null && _a !== void 0 ? _a : 'fail') === 'skip';
217
+ const listed = await (0, rebuildHelpers_1.invokeHook)(() => source.list());
171
218
  if (listed.isFailure()) {
172
- return (0, ts_utils_1.fail)(`fragment index rebuild: failed to list records: ${listed.message}`);
219
+ // Deliberately BEFORE the reset, matching the record-granular sibling: a
220
+ // failed list is no evidence about the fragments already held, and nothing
221
+ // has been re-embedded yet, so there is no half-rebuilt state to guard
222
+ // against. Discarding a healthy index over a transient read error is data
223
+ // loss, not caution. No detail either — an all-zero report would describe
224
+ // an index this call never touched.
225
+ return (0, ts_utils_1.failWithDetail)(`fragment index rebuild: failed to list records: ${listed.message}`);
173
226
  }
174
- for (const scoped of listed.value) {
175
- const embedded = await embed(scoped.record);
227
+ // From here a rebuild is genuinely starting, so clear. A mid-loop failure
228
+ // under `'fail'` still resets, which is what keeps that contract honest.
229
+ this._reset();
230
+ const indexed = new Map();
231
+ const fragments = new Map();
232
+ const declined = new Map();
233
+ const skipped = [];
234
+ // Only the source knows what it filtered, so an absent `excluded` propagates
235
+ // as absent rather than becoming an empty map. Before this contract existed
236
+ // the fragment path dropped this tally on the floor, having nowhere honest to
237
+ // put it.
238
+ const report = () => ({
239
+ indexed,
240
+ fragments,
241
+ declined,
242
+ excluded: listed.value.excluded,
243
+ skipped
244
+ });
245
+ for (const scoped of listed.value.records) {
246
+ const kind = scoped.record.envelope.kind;
247
+ // Consumer-supplied, so a throw or rejection is captured rather than
248
+ // escaping mid-loop and leaving the index half-populated.
249
+ const embedded = await (0, rebuildHelpers_1.invokeHook)(() => embed(scoped.record));
176
250
  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}`);
251
+ const error = `fragment index rebuild: embedding '${(0, types_1.edgeTargetKey)(scoped.target)}' failed: ${embedded.message}`;
252
+ if (!lenient) {
253
+ this._reset();
254
+ return (0, ts_utils_1.failWithDetail)(error, report());
255
+ }
256
+ skipped.push({ target: scoped.target, error });
257
+ continue;
179
258
  }
259
+ // An empty array is this lane's decline. It still performs a real
260
+ // whole-record-replace — which is what clears any stale fragments — so it
261
+ // is written, then counted as declined rather than indexed.
180
262
  const added = await this.addFragments(scoped.target, embedded.value);
181
263
  if (added.isFailure()) {
182
- this._reset();
183
- return (0, ts_utils_1.fail)(`fragment index rebuild: ${added.message}`);
264
+ const error = `fragment index rebuild: ${added.message}`;
265
+ if (!lenient) {
266
+ this._reset();
267
+ return (0, ts_utils_1.failWithDetail)(error, report());
268
+ }
269
+ skipped.push({ target: scoped.target, error });
270
+ continue;
271
+ }
272
+ if (added.value === 0) {
273
+ (0, rebuildHelpers_1.tally)(declined, kind);
274
+ continue;
184
275
  }
276
+ // Tallied per successful add rather than read back off the counts at the
277
+ // end, so the per-kind buckets line up with their siblings.
278
+ (0, rebuildHelpers_1.tally)(indexed, kind);
279
+ (0, rebuildHelpers_1.tally)(fragments, kind, added.value);
185
280
  }
186
- return (0, ts_utils_1.succeed)(this.fragmentCount);
281
+ return (0, ts_utils_1.succeedWithDetail)(report());
187
282
  }
188
283
  /** Empty the index and forget the established dimension. */
189
284
  _reset() {