@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -1 +1 @@
1
- {"version":3,"file":"hybridRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/hybridRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n limitRecords,\n recencyCompare\n} from './retriever';\n\n/**\n * Strategy for combining the result sets of the retrievers a\n * {@link HybridRetriever} composes. Injectable so a consumer can weight,\n * re-rank, or intersect instead of the default union.\n * @public\n */\nexport interface IMergeStrategy {\n /**\n * Merge the per-retriever result sets into a single ordered result.\n * @param resultSets - One entry per composed retriever, in composition order.\n */\n merge(\n resultSets: ReadonlyArray<ReadonlyArray<IMemoryRecord<unknown>>>\n ): Result<ReadonlyArray<IMemoryRecord<unknown>>>;\n}\n\n/**\n * The reference {@link IMergeStrategy}: a score-union. Every record is scored by\n * the number of composed result sets it appears in (deduplicated by\n * {@link IMemoryEnvelope.id | id}); the merged result is ordered by descending\n * score, then by recency. Records surfaced by more retrievers rank higher.\n *\n * @remarks\n * Dedup is by `id` alone, which is unambiguous for the flat (one-file-per-entity)\n * kinds B2 ships. Phase-C versioned kinds that reuse a stem across scopes will\n * extend the dedup key — additive, no API change.\n * @public\n */\nexport class ScoreUnionMergeStrategy implements IMergeStrategy {\n private constructor() {}\n\n /** Family-convention factory. */\n public static create(): Result<ScoreUnionMergeStrategy> {\n return succeed(new ScoreUnionMergeStrategy());\n }\n\n /** {@inheritDoc IMergeStrategy.merge} */\n public merge(\n resultSets: ReadonlyArray<ReadonlyArray<IMemoryRecord<unknown>>>\n ): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const scored: Map<MemoryId, { readonly record: IMemoryRecord<unknown>; score: number }> = new Map();\n for (const set of resultSets) {\n // Count each id at most once per result set, so the score is \"how many\n // retrievers surfaced this record\" rather than \"how many copies total\".\n const seenInSet: Set<MemoryId> = new Set<MemoryId>();\n for (const record of set) {\n const id: MemoryId = record.envelope.id;\n if (seenInSet.has(id)) {\n continue;\n }\n seenInSet.add(id);\n const existing = scored.get(id);\n if (existing === undefined) {\n scored.set(id, { record, score: 1 });\n } else {\n existing.score += 1;\n }\n }\n }\n const merged: IMemoryRecord<unknown>[] = Array.from(scored.values())\n .sort((a, b) => (b.score !== a.score ? b.score - a.score : recencyCompare(a.record, b.record)))\n .map((entry) => entry.record);\n return succeed(merged);\n }\n}\n\n/**\n * Composes several retrievers, dispatches a query to each, and merges their\n * results via an injectable {@link IMergeStrategy}. Its capabilities are the\n * union of the composed retrievers' capabilities.\n *\n * @remarks\n * The hybrid enforces the loud-degradation contract against its OWN (union)\n * capabilities, then projects the query for each child: a child that does not\n * support the `semantic` (or `asOf`) axis is handed a query with that axis\n * stripped, so it returns its normal results instead of loud-failing on a field\n * a sibling handles. A child that genuinely fails (e.g. a wired semantic backend\n * erroring) propagates — the hybrid never silently drops a failure.\n * @public\n */\nexport class HybridRetriever implements IMemoryRetriever {\n private readonly _retrievers: ReadonlyArray<IMemoryRetriever>;\n private readonly _mergeStrategy: IMergeStrategy;\n private readonly _capabilities: IMemoryRetrieverCapabilities;\n\n private constructor(\n retrievers: ReadonlyArray<IMemoryRetriever>,\n mergeStrategy: IMergeStrategy,\n capabilities: IMemoryRetrieverCapabilities\n ) {\n this._retrievers = retrievers;\n this._mergeStrategy = mergeStrategy;\n this._capabilities = capabilities;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return this._capabilities;\n }\n\n /**\n * Family-convention factory.\n * @param retrievers - The retrievers to compose (at least one).\n * @param mergeStrategy - How to combine their results.\n */\n public static create(\n retrievers: ReadonlyArray<IMemoryRetriever>,\n mergeStrategy: IMergeStrategy\n ): Result<HybridRetriever> {\n if (retrievers.length === 0) {\n return fail('HybridRetriever: at least one retriever is required');\n }\n // Snapshot the caller's array so a later mutation cannot make `retrieve()`\n // and the cached `capabilities` disagree.\n const stableRetrievers: ReadonlyArray<IMemoryRetriever> = [...retrievers];\n const capabilities: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: stableRetrievers.some((r) => r.capabilities.supportsSemanticRecall),\n supportsTemporalQuery: stableRetrievers.some((r) => r.capabilities.supportsTemporalQuery),\n supportsLinkTraversal: stableRetrievers.some((r) => r.capabilities.supportsLinkTraversal)\n };\n return succeed(new HybridRetriever(stableRetrievers, mergeStrategy, capabilities));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return guardRetrieverCapabilities(query, this._capabilities).thenOnSuccess(async () => {\n const perRetriever: Result<ReadonlyArray<IMemoryRecord<unknown>>>[] = await Promise.all(\n this._retrievers.map((retriever) => retriever.retrieve(this._projectQuery(query, retriever)))\n );\n return mapResults(perRetriever)\n .onSuccess((resultSets) => this._mergeStrategy.merge(resultSets))\n .onSuccess((merged) => succeed(limitRecords(merged, query.limit)));\n });\n }\n\n /**\n * Project the query for one child retriever. Two adjustments:\n *\n * - Strip axes the child does not support (`semantic` / `topK` for a\n * non-semantic child, `asOf` for a non-temporal child, the link axes for a\n * non-link child) so it returns its normal results rather than loud-failing\n * on a field a sibling handles.\n * - Strip `limit` unconditionally: limit is a post-merge concern. A child that\n * pre-truncated its result set would starve the merge strategy of candidates\n * it needs to score correctly (a record both children would surface must\n * reach the merge to score 2). The hybrid applies `limit` once, after merge.\n */\n private _projectQuery(query: IMemoryQuery, retriever: IMemoryRetriever): IMemoryQuery {\n const projected: { -readonly [K in keyof IMemoryQuery]: IMemoryQuery[K] } = { ...query };\n delete projected.limit;\n if (!retriever.capabilities.supportsSemanticRecall) {\n delete projected.semantic;\n delete projected.topK;\n }\n if (!retriever.capabilities.supportsTemporalQuery) {\n delete projected.asOf;\n }\n if (!retriever.capabilities.supportsLinkTraversal) {\n delete projected.linkedFrom;\n delete projected.linkedTo;\n delete projected.hops;\n }\n return projected;\n }\n}\n"]}
1
+ {"version":3,"file":"hybridRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/hybridRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, mapResults, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n limitRecords,\n rankCompare,\n recencyCompare\n} from './retriever';\n\n/**\n * Strategy for combining the result sets of the retrievers a\n * {@link HybridRetriever} composes. Injectable so a consumer can weight,\n * re-rank, or intersect instead of the default union.\n * @public\n */\nexport interface IMergeStrategy {\n /**\n * Merge the per-retriever result sets into a single ordered result.\n * @param resultSets - One entry per composed retriever, in composition order.\n */\n merge(\n resultSets: ReadonlyArray<ReadonlyArray<IMemoryRecord<unknown>>>\n ): Result<ReadonlyArray<IMemoryRecord<unknown>>>;\n}\n\n/**\n * The reference {@link IMergeStrategy}: a score-union. Every record is scored by\n * the number of composed result sets it appears in (deduplicated by\n * {@link IMemoryEnvelope.id | id}); the merged result is ordered by descending\n * score, then by recency. Records surfaced by more retrievers rank higher.\n *\n * @remarks\n * Dedup is by `id` alone, which is unambiguous for the flat (one-file-per-entity)\n * kinds B2 ships. Phase-C versioned kinds that reuse a stem across scopes will\n * extend the dedup key — additive, no API change.\n * @public\n */\nexport class ScoreUnionMergeStrategy implements IMergeStrategy {\n private constructor() {}\n\n /** Family-convention factory. */\n public static create(): Result<ScoreUnionMergeStrategy> {\n return succeed(new ScoreUnionMergeStrategy());\n }\n\n /** {@inheritDoc IMergeStrategy.merge} */\n public merge(\n resultSets: ReadonlyArray<ReadonlyArray<IMemoryRecord<unknown>>>\n ): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const scored: Map<MemoryId, { readonly record: IMemoryRecord<unknown>; score: number }> = new Map();\n for (const set of resultSets) {\n // Count each id at most once per result set, so the score is \"how many\n // retrievers surfaced this record\" rather than \"how many copies total\".\n const seenInSet: Set<MemoryId> = new Set<MemoryId>();\n for (const record of set) {\n const id: MemoryId = record.envelope.id;\n if (seenInSet.has(id)) {\n continue;\n }\n seenInSet.add(id);\n const existing = scored.get(id);\n if (existing === undefined) {\n scored.set(id, { record, score: 1 });\n } else {\n existing.score += 1;\n }\n }\n }\n const merged: IMemoryRecord<unknown>[] = Array.from(scored.values())\n .sort((a, b) => (b.score !== a.score ? b.score - a.score : recencyCompare(a.record, b.record)))\n .map((entry) => entry.record);\n return succeed(merged);\n }\n}\n\n/**\n * Composes several retrievers, dispatches a query to each, and merges their\n * results via an injectable {@link IMergeStrategy}. Its capabilities are the\n * union of the composed retrievers' capabilities.\n *\n * @remarks\n * The hybrid enforces the loud-degradation contract against its OWN (union)\n * capabilities, then projects the query for each child: a child that does not\n * support the `semantic` (or `asOf`) axis is handed a query with that axis\n * stripped, so it returns its normal results instead of loud-failing on a field\n * a sibling handles. A child that genuinely fails (e.g. a wired semantic backend\n * erroring) propagates — the hybrid never silently drops a failure.\n * @public\n */\nexport class HybridRetriever implements IMemoryRetriever {\n private readonly _retrievers: ReadonlyArray<IMemoryRetriever>;\n private readonly _mergeStrategy: IMergeStrategy;\n private readonly _capabilities: IMemoryRetrieverCapabilities;\n\n private constructor(\n retrievers: ReadonlyArray<IMemoryRetriever>,\n mergeStrategy: IMergeStrategy,\n capabilities: IMemoryRetrieverCapabilities\n ) {\n this._retrievers = retrievers;\n this._mergeStrategy = mergeStrategy;\n this._capabilities = capabilities;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return this._capabilities;\n }\n\n /**\n * Family-convention factory.\n * @param retrievers - The retrievers to compose (at least one).\n * @param mergeStrategy - How to combine their results.\n */\n public static create(\n retrievers: ReadonlyArray<IMemoryRetriever>,\n mergeStrategy: IMergeStrategy\n ): Result<HybridRetriever> {\n if (retrievers.length === 0) {\n return fail('HybridRetriever: at least one retriever is required');\n }\n // Snapshot the caller's array so a later mutation cannot make `retrieve()`\n // and the cached `capabilities` disagree.\n const stableRetrievers: ReadonlyArray<IMemoryRetriever> = [...retrievers];\n const capabilities: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: stableRetrievers.some((r) => r.capabilities.supportsSemanticRecall),\n supportsTemporalQuery: stableRetrievers.some((r) => r.capabilities.supportsTemporalQuery),\n supportsLinkTraversal: stableRetrievers.some((r) => r.capabilities.supportsLinkTraversal)\n };\n return succeed(new HybridRetriever(stableRetrievers, mergeStrategy, capabilities));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return guardRetrieverCapabilities(query, this._capabilities).thenOnSuccess(async () => {\n const perRetriever: Result<ReadonlyArray<IMemoryRecord<unknown>>>[] = await Promise.all(\n this._retrievers.map((retriever) => retriever.retrieve(this._projectQuery(query, retriever)))\n );\n return mapResults(perRetriever)\n .onSuccess((resultSets) => this._mergeStrategy.merge(resultSets))\n .onSuccess((merged) => {\n // `orderBy: 'rank'` re-orders the merged set by rank (descending, absent\n // last) before the page window, so a rank-ordered hybrid query yields a\n // rank-ordered page. Absent / `'recency'` preserves the merge strategy's\n // own ordering (byte-identical to the pre-`orderBy` behavior).\n const ordered: ReadonlyArray<IMemoryRecord<unknown>> =\n query.orderBy === 'rank' ? [...merged].sort(rankCompare) : merged;\n return succeed(limitRecords(ordered, query.limit, query.offset));\n });\n });\n }\n\n /**\n * Project the query for one child retriever. Two adjustments:\n *\n * - Strip axes the child does not support (`semantic` / `topK` for a\n * non-semantic child, `asOf` for a non-temporal child, the link axes for a\n * non-link child) so it returns its normal results rather than loud-failing\n * on a field a sibling handles.\n * - Strip `limit` unconditionally: limit is a post-merge concern. A child that\n * pre-truncated its result set would starve the merge strategy of candidates\n * it needs to score correctly (a record both children would surface must\n * reach the merge to score 2). The hybrid applies `limit` once, after merge.\n */\n private _projectQuery(query: IMemoryQuery, retriever: IMemoryRetriever): IMemoryQuery {\n const projected: { -readonly [K in keyof IMemoryQuery]: IMemoryQuery[K] } = { ...query };\n delete projected.limit;\n // Offset, like limit, is a post-merge concern: a child that pre-skipped its\n // own ordered set would drop candidates the merge needs to score correctly.\n // The hybrid applies the `{ offset, limit }` window once, after merge.\n delete projected.offset;\n if (!retriever.capabilities.supportsSemanticRecall) {\n delete projected.semantic;\n delete projected.topK;\n }\n if (!retriever.capabilities.supportsTemporalQuery) {\n delete projected.asOf;\n }\n if (!retriever.capabilities.supportsLinkTraversal) {\n delete projected.linkedFrom;\n delete projected.linkedTo;\n delete projected.hops;\n }\n return projected;\n }\n}\n"]}
@@ -3,7 +3,8 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { fail, succeed } from '@fgv/ts-utils';
6
- import { guardRetrieverCapabilities, indexedRecordMatchesQuery, limitRecords, recencyCompare } from './retriever';
6
+ import { edgeTargetKey } from '../types';
7
+ import { guardRetrieverCapabilities, indexedRecordMatchesQuery, limitRecords, orderingCompare } from './retriever';
7
8
  /** The capabilities a link-traversal retriever exposes (link traversal only). */
8
9
  const LINK_TRAVERSAL_CAPABILITIES = {
9
10
  supportsSemanticRecall: false,
@@ -19,20 +20,23 @@ const DEFAULT_HOPS = 1;
19
20
  */
20
21
  export const LINK_TRAVERSAL_NO_SEED_MESSAGE = 'link traversal requires a seed id (linkedFrom or linkedTo)';
21
22
  /**
22
- * Breadth-first link-traversal retriever. From a seed {@link MemoryId} it walks
23
- * the link graph up to `query.hops` levels and returns the records reached
24
- * (excluding the seed), recency-ordered and limited.
23
+ * Breadth-first link-traversal retriever. From a scope-qualified
24
+ * {@link IEdgeTarget} seed it walks the link graph up to `query.hops` levels and
25
+ * returns the records reached (excluding the seed), recency-ordered and limited.
25
26
  *
26
27
  * @remarks
27
28
  * - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's
28
29
  * `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's
29
30
  * `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.
31
+ * - **Scope-qualified nodes.** Every graph node is an {@link IEdgeTarget}
32
+ * `(scope, id)` pair, so following an edge to `turn-3` reaches ONLY the record
33
+ * in the edge's own scope — never a same-stem record in another scope.
30
34
  * - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a
31
- * single hop) and a visited-set guard. The graph is keyed by bare
32
- * string {@link MemoryId}s, so a `Set<string>` visited-set is the exact,
33
- * collision-free cycle key — no structural hashing (e.g. `Crc32Normalizer`) is
34
- * needed. A self-loop or any multi-hop cycle terminates because a revisited id
35
- * is never re-expanded.
35
+ * single hop) and a visited-set guard. Nodes are canonicalized to their
36
+ * `(scope, id)` string via {@link edgeTargetKey}, so a `Set<string>` visited-set
37
+ * is the exact, collision-free cycle key — no structural hashing (e.g.
38
+ * `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates
39
+ * because a revisited node is never re-expanded.
36
40
  * - **Post-filter.** The scope / kind / tag / predicate axes of the query are
37
41
  * applied to the reached records (the link axes are the traversal itself).
38
42
  * @public
@@ -62,18 +66,20 @@ export class LinkTraversalRetriever {
62
66
  return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);
63
67
  }
64
68
  const hops = (_b = query.hops) !== null && _b !== void 0 ? _b : DEFAULT_HOPS;
65
- const byId = this._indexById();
66
- // The visited-set IS the cycle guard: ids are strings, so set membership is
67
- // an exact identity check. The seed is pre-marked so it is never re-added.
68
- const visited = new Set([seed]);
69
+ const byKey = this._indexByKey();
70
+ // The visited-set IS the cycle guard: nodes are canonicalized to their
71
+ // `(scope, id)` string, so set membership is an exact identity check. The
72
+ // seed is pre-marked so it is never re-added.
73
+ const visited = new Set([edgeTargetKey(seed)]);
69
74
  const reached = [];
70
75
  let frontier = [seed];
71
76
  for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
72
77
  const next = [];
73
- for (const id of frontier) {
74
- for (const neighbor of outbound ? this._outbound(id, byId) : this._inbound(id)) {
75
- if (!visited.has(neighbor)) {
76
- visited.add(neighbor);
78
+ for (const node of frontier) {
79
+ for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {
80
+ const neighborKey = edgeTargetKey(neighbor);
81
+ if (!visited.has(neighborKey)) {
82
+ visited.add(neighborKey);
77
83
  reached.push(neighbor);
78
84
  next.push(neighbor);
79
85
  }
@@ -82,62 +88,45 @@ export class LinkTraversalRetriever {
82
88
  frontier = next;
83
89
  }
84
90
  const entries = [];
85
- for (const id of reached) {
86
- const matches = byId.get(id);
87
- if (matches !== undefined) {
88
- entries.push(...matches);
91
+ for (const node of reached) {
92
+ const match = byKey.get(edgeTargetKey(node));
93
+ if (match !== undefined) {
94
+ entries.push(match);
89
95
  }
90
96
  }
91
97
  const ordered = entries
92
98
  .filter((entry) => indexedRecordMatchesQuery(entry, query))
93
99
  .map((entry) => entry.record)
94
- .sort(recencyCompare);
95
- return succeed(limitRecords(ordered, query.limit));
100
+ .sort(orderingCompare(query.orderBy));
101
+ return succeed(limitRecords(ordered, query.limit, query.offset));
96
102
  }
97
103
  /**
98
- * Group the index's entries by bare {@link MemoryId}. An id can map to more
99
- * than one entry when distinct scopes reuse a filename stem (e.g. `turn-0` in
100
- * two conversations), so the value is an array.
101
- *
102
- * @remarks
103
- * **Design note (links are globally-scoped identifiers in this phase).** An
104
- * {@link IEdge.target} is a bare `MemoryId`, not a `(scope, id)` pair, so
105
- * traversal resolves a target across ALL scopes that hold that id. When two
106
- * scopes reuse a stem, following an edge to it reaches every match. This
107
- * mirrors the `backlinks` index, which is also keyed by bare id. Scope-
108
- * qualified link resolution is intentionally out of scope for Phase C and
109
- * would be an additive change here (and to {@link IEdge} / the index).
104
+ * Group the index's entries by their scope-qualified {@link edgeTargetKey}
105
+ * `(scope, id)` composite. Each composite is the index's primary key, so it maps
106
+ * to exactly one entry — two records that reuse a filename stem across scopes
107
+ * (e.g. `turn-0` in two conversations) get distinct keys and never collide.
110
108
  */
111
- _indexById() {
112
- const byId = new Map();
109
+ _indexByKey() {
110
+ const byKey = new Map();
113
111
  for (const entry of this._index.entries()) {
114
- const id = entry.record.envelope.id;
115
- const existing = byId.get(id);
116
- if (existing === undefined) {
117
- byId.set(id, [entry]);
118
- }
119
- else {
120
- existing.push(entry);
121
- }
112
+ byKey.set(edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry);
122
113
  }
123
- return byId;
114
+ return byKey;
124
115
  }
125
- /** Outbound neighbors: the targets of every edge on the records with this id. */
126
- _outbound(id, byId) {
116
+ /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */
117
+ _outbound(node, byKey) {
127
118
  const targets = [];
128
- const matches = byId.get(id);
129
- if (matches !== undefined) {
130
- for (const entry of matches) {
131
- for (const edge of entry.record.envelope.links) {
132
- targets.push(edge.target);
133
- }
119
+ const match = byKey.get(edgeTargetKey(node));
120
+ if (match !== undefined) {
121
+ for (const edge of match.record.envelope.links) {
122
+ targets.push(edge.target);
134
123
  }
135
124
  }
136
125
  return targets;
137
126
  }
138
- /** Inbound neighbors: the ids whose edges point AT this id (the backlinks). */
139
- _inbound(id) {
140
- return this._index.backlinks(id);
127
+ /** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */
128
+ _inbound(node) {
129
+ return this._index.backlinks(node);
141
130
  }
142
131
  }
143
132
  //# sourceMappingURL=linkTraversalRetriever.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"linkTraversalRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n recencyCompare\n} from './retriever';\n\n/** The capabilities a link-traversal retriever exposes (link traversal only). */\nconst LINK_TRAVERSAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: true\n};\n\n/** Default BFS hop count when `query.hops` is not supplied. */\nconst DEFAULT_HOPS: number = 1;\n\n/**\n * The loud-degradation message returned when a link-traversal query supplies no\n * seed (`linkedFrom` / `linkedTo`).\n * @public\n */\nexport const LINK_TRAVERSAL_NO_SEED_MESSAGE: string =\n 'link traversal requires a seed id (linkedFrom or linkedTo)';\n\n/**\n * Breadth-first link-traversal retriever. From a seed {@link MemoryId} it walks\n * the link graph up to `query.hops` levels and returns the records reached\n * (excluding the seed), recency-ordered and limited.\n *\n * @remarks\n * - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's\n * `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's\n * `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.\n * - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a\n * single hop) and a visited-set guard. The graph is keyed by bare\n * string {@link MemoryId}s, so a `Set<string>` visited-set is the exact,\n * collision-free cycle key — no structural hashing (e.g. `Crc32Normalizer`) is\n * needed. A self-loop or any multi-hop cycle terminates because a revisited id\n * is never re-expanded.\n * - **Post-filter.** The scope / kind / tag / predicate axes of the query are\n * applied to the reached records (the link axes are the traversal itself).\n * @public\n */\nexport class LinkTraversalRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<LinkTraversalRetriever> {\n return succeed(new LinkTraversalRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return LINK_TRAVERSAL_CAPABILITIES;\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => this._traverse(query))\n );\n }\n\n /** Run the bounded, cycle-safe BFS and post-filter the reached records. */\n private _traverse(query: IMemoryQuery): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const outbound: boolean = query.linkedFrom !== undefined;\n const seed: MemoryId | undefined = query.linkedFrom ?? query.linkedTo;\n if (seed === undefined) {\n return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);\n }\n const hops: number = query.hops ?? DEFAULT_HOPS;\n const byId: ReadonlyMap<MemoryId, IIndexedMemoryRecord[]> = this._indexById();\n\n // The visited-set IS the cycle guard: ids are strings, so set membership is\n // an exact identity check. The seed is pre-marked so it is never re-added.\n const visited: Set<string> = new Set<string>([seed]);\n const reached: MemoryId[] = [];\n let frontier: MemoryId[] = [seed];\n for (let hop = 0; hop < hops && frontier.length > 0; hop++) {\n const next: MemoryId[] = [];\n for (const id of frontier) {\n for (const neighbor of outbound ? this._outbound(id, byId) : this._inbound(id)) {\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n reached.push(neighbor);\n next.push(neighbor);\n }\n }\n }\n frontier = next;\n }\n\n const entries: IIndexedMemoryRecord[] = [];\n for (const id of reached) {\n const matches: IIndexedMemoryRecord[] | undefined = byId.get(id);\n if (matches !== undefined) {\n entries.push(...matches);\n }\n }\n const ordered: IMemoryRecord<unknown>[] = entries\n .filter((entry) => indexedRecordMatchesQuery(entry, query))\n .map((entry) => entry.record)\n .sort(recencyCompare);\n return succeed(limitRecords(ordered, query.limit));\n }\n\n /**\n * Group the index's entries by bare {@link MemoryId}. An id can map to more\n * than one entry when distinct scopes reuse a filename stem (e.g. `turn-0` in\n * two conversations), so the value is an array.\n *\n * @remarks\n * **Design note (links are globally-scoped identifiers in this phase).** An\n * {@link IEdge.target} is a bare `MemoryId`, not a `(scope, id)` pair, so\n * traversal resolves a target across ALL scopes that hold that id. When two\n * scopes reuse a stem, following an edge to it reaches every match. This\n * mirrors the `backlinks` index, which is also keyed by bare id. Scope-\n * qualified link resolution is intentionally out of scope for Phase C and\n * would be an additive change here (and to {@link IEdge} / the index).\n */\n private _indexById(): ReadonlyMap<MemoryId, IIndexedMemoryRecord[]> {\n const byId: Map<MemoryId, IIndexedMemoryRecord[]> = new Map<MemoryId, IIndexedMemoryRecord[]>();\n for (const entry of this._index.entries()) {\n const id: MemoryId = entry.record.envelope.id;\n const existing: IIndexedMemoryRecord[] | undefined = byId.get(id);\n if (existing === undefined) {\n byId.set(id, [entry]);\n } else {\n existing.push(entry);\n }\n }\n return byId;\n }\n\n /** Outbound neighbors: the targets of every edge on the records with this id. */\n private _outbound(id: MemoryId, byId: ReadonlyMap<MemoryId, IIndexedMemoryRecord[]>): MemoryId[] {\n const targets: MemoryId[] = [];\n const matches: IIndexedMemoryRecord[] | undefined = byId.get(id);\n if (matches !== undefined) {\n for (const entry of matches) {\n for (const edge of entry.record.envelope.links) {\n targets.push(edge.target);\n }\n }\n }\n return targets;\n }\n\n /** Inbound neighbors: the ids whose edges point AT this id (the backlinks). */\n private _inbound(id: MemoryId): ReadonlyArray<MemoryId> {\n return this._index.backlinks(id);\n }\n}\n"]}
1
+ {"version":3,"file":"linkTraversalRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, edgeTargetKey } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n limitRecords,\n orderingCompare\n} from './retriever';\n\n/** The capabilities a link-traversal retriever exposes (link traversal only). */\nconst LINK_TRAVERSAL_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: true\n};\n\n/** Default BFS hop count when `query.hops` is not supplied. */\nconst DEFAULT_HOPS: number = 1;\n\n/**\n * The loud-degradation message returned when a link-traversal query supplies no\n * seed (`linkedFrom` / `linkedTo`).\n * @public\n */\nexport const LINK_TRAVERSAL_NO_SEED_MESSAGE: string =\n 'link traversal requires a seed id (linkedFrom or linkedTo)';\n\n/**\n * Breadth-first link-traversal retriever. From a scope-qualified\n * {@link IEdgeTarget} seed it walks the link graph up to `query.hops` levels and\n * returns the records reached (excluding the seed), recency-ordered and limited.\n *\n * @remarks\n * - **Direction.** `linkedFrom` walks OUTBOUND edges (each record's\n * `envelope.links[].target`); `linkedTo` walks INBOUND edges (the index's\n * `backlinks`). Exactly one is the seed; `linkedFrom` wins if both are set.\n * - **Scope-qualified nodes.** Every graph node is an {@link IEdgeTarget}\n * `(scope, id)` pair, so following an edge to `turn-3` reaches ONLY the record\n * in the edge's own scope — never a same-stem record in another scope.\n * - **Bound + cycle safety.** Traversal is bounded by `hops` (default `1` — a\n * single hop) and a visited-set guard. Nodes are canonicalized to their\n * `(scope, id)` string via {@link edgeTargetKey}, so a `Set<string>` visited-set\n * is the exact, collision-free cycle key — no structural hashing (e.g.\n * `Crc32Normalizer`) is needed. A self-loop or any multi-hop cycle terminates\n * because a revisited node is never re-expanded.\n * - **Post-filter.** The scope / kind / tag / predicate axes of the query are\n * applied to the reached records (the link axes are the traversal itself).\n * @public\n */\nexport class LinkTraversalRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<LinkTraversalRetriever> {\n return succeed(new LinkTraversalRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return LINK_TRAVERSAL_CAPABILITIES;\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => this._traverse(query))\n );\n }\n\n /** Run the bounded, cycle-safe BFS and post-filter the reached records. */\n private _traverse(query: IMemoryQuery): Result<ReadonlyArray<IMemoryRecord<unknown>>> {\n const outbound: boolean = query.linkedFrom !== undefined;\n const seed: IEdgeTarget | undefined = query.linkedFrom ?? query.linkedTo;\n if (seed === undefined) {\n return fail(LINK_TRAVERSAL_NO_SEED_MESSAGE);\n }\n const hops: number = query.hops ?? DEFAULT_HOPS;\n const byKey: ReadonlyMap<string, IIndexedMemoryRecord> = this._indexByKey();\n\n // The visited-set IS the cycle guard: nodes are canonicalized to their\n // `(scope, id)` string, so set membership is an exact identity check. The\n // seed is pre-marked so it is never re-added.\n const visited: Set<string> = new Set<string>([edgeTargetKey(seed)]);\n const reached: IEdgeTarget[] = [];\n let frontier: IEdgeTarget[] = [seed];\n for (let hop = 0; hop < hops && frontier.length > 0; hop++) {\n const next: IEdgeTarget[] = [];\n for (const node of frontier) {\n for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {\n const neighborKey: string = edgeTargetKey(neighbor);\n if (!visited.has(neighborKey)) {\n visited.add(neighborKey);\n reached.push(neighbor);\n next.push(neighbor);\n }\n }\n }\n frontier = next;\n }\n\n const entries: IIndexedMemoryRecord[] = [];\n for (const node of reached) {\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n entries.push(match);\n }\n }\n const ordered: IMemoryRecord<unknown>[] = entries\n .filter((entry) => indexedRecordMatchesQuery(entry, query))\n .map((entry) => entry.record)\n .sort(orderingCompare(query.orderBy));\n return succeed(limitRecords(ordered, query.limit, query.offset));\n }\n\n /**\n * Group the index's entries by their scope-qualified {@link edgeTargetKey}\n * `(scope, id)` composite. Each composite is the index's primary key, so it maps\n * to exactly one entry — two records that reuse a filename stem across scopes\n * (e.g. `turn-0` in two conversations) get distinct keys and never collide.\n */\n private _indexByKey(): ReadonlyMap<string, IIndexedMemoryRecord> {\n const byKey: Map<string, IIndexedMemoryRecord> = new Map<string, IIndexedMemoryRecord>();\n for (const entry of this._index.entries()) {\n byKey.set(edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry);\n }\n return byKey;\n }\n\n /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */\n private _outbound(node: IEdgeTarget, byKey: ReadonlyMap<string, IIndexedMemoryRecord>): IEdgeTarget[] {\n const targets: IEdgeTarget[] = [];\n const match: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(node));\n if (match !== undefined) {\n for (const edge of match.record.envelope.links) {\n targets.push(edge.target);\n }\n }\n return targets;\n }\n\n /** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */\n private _inbound(node: IEdgeTarget): ReadonlyArray<IEdgeTarget> {\n return this._index.backlinks(node);\n }\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { succeed } from '@fgv/ts-utils';
6
- import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, recencyCompare, selectByQuery } from './retriever';
6
+ import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
7
7
  /**
8
8
  * Returns records matching the query's scope / kind / tag / predicate filters,
9
9
  * ordered most-recently-updated first. The universal v1 retriever — an empty
@@ -25,8 +25,8 @@ export class RecencyRetriever {
25
25
  /** {@inheritDoc IMemoryRetriever.retrieve} */
26
26
  retrieve(query) {
27
27
  return Promise.resolve(guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {
28
- const ordered = selectByQuery(this._index.entries(), query).sort(recencyCompare);
29
- return succeed(limitRecords(ordered, query.limit));
28
+ const ordered = selectByQuery(this._index.entries(), query).sort(orderingCompare(query.orderBy));
29
+ return succeed(limitRecords(ordered, query.limit, query.offset));
30
30
  }));
31
31
  }
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,cAAc,EACd,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,cAAc,CACf,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n recencyCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching the query's scope / kind / tag / predicate filters,\n * ordered most-recently-updated first. The universal v1 retriever — an empty\n * query returns the whole vault in recency order.\n * @public\n */\nexport class RecencyRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<RecencyRetriever> {\n return succeed(new RecencyRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n recencyCompare\n );\n return succeed(limitRecords(ordered, query.limit));\n })\n );\n }\n}\n"]}
1
+ {"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching the query's scope / kind / tag / predicate filters,\n * ordered most-recently-updated first. The universal v1 retriever — an empty\n * query returns the whole vault in recency order.\n * @public\n */\nexport class RecencyRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<RecencyRetriever> {\n return succeed(new RecencyRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
@@ -62,6 +62,36 @@ export function recencyCompare(a, b) {
62
62
  const byUpdated = b.envelope.updated - a.envelope.updated;
63
63
  return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;
64
64
  }
65
+ /**
66
+ * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with
67
+ * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST
68
+ * (after every ranked record), then by recency among themselves. Mirrors the
69
+ * index's rank-view ordering.
70
+ * @public
71
+ */
72
+ export function rankCompare(a, b) {
73
+ const ra = a.envelope.rank;
74
+ const rb = b.envelope.rank;
75
+ if (ra === undefined && rb !== undefined) {
76
+ return 1;
77
+ }
78
+ if (rb === undefined && ra !== undefined) {
79
+ return -1;
80
+ }
81
+ if (ra !== undefined && rb !== undefined && ra !== rb) {
82
+ return rb - ra;
83
+ }
84
+ return recencyCompare(a, b);
85
+ }
86
+ /**
87
+ * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}
88
+ * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the
89
+ * default, byte-identical to the pre-`orderBy` behavior).
90
+ * @public
91
+ */
92
+ export function orderingCompare(orderBy) {
93
+ return orderBy === 'rank' ? rankCompare : recencyCompare;
94
+ }
65
95
  /**
66
96
  * Whether an indexed entry satisfies a query's scope / kind / tag / predicate
67
97
  * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /
@@ -75,6 +105,9 @@ export function indexedRecordMatchesQuery(entry, query) {
75
105
  if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {
76
106
  return false;
77
107
  }
108
+ if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {
109
+ return false;
110
+ }
78
111
  if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
79
112
  return false;
80
113
  }
@@ -92,19 +125,29 @@ export function selectByQuery(entries, query) {
92
125
  return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);
93
126
  }
94
127
  /**
95
- * Truncate to `query.limit` records (a no-op when `limit` is absent). Applied
96
- * last, after ordering, so it always takes the top-N of the ordered result. A
97
- * non-positive `limit` is public query input and means "no records" it returns
98
- * an empty array rather than letting a negative value slip into `slice`.
128
+ * Apply the `{ offset, limit }` page window to an ordered record set. Applied
129
+ * last, after ordering, so it always takes a stable window of the ordered
130
+ * result. `offset` is applied first (records to skip), then `limit` (top-N of
131
+ * the remainder).
132
+ *
133
+ * @remarks
134
+ * Both bounds are public query input and are guarded against non-positive
135
+ * values slipping into `slice`:
136
+ * - `offset` absent or non-positive → no skip (today's behavior). An offset past
137
+ * the end yields an empty page rather than a throw.
138
+ * - `limit` absent → no truncation; a non-positive `limit` means "no records"
139
+ * and returns an empty array.
99
140
  * @public
100
141
  */
101
- export function limitRecords(records, limit) {
142
+ export function limitRecords(records, limit, offset) {
143
+ const skip = offset !== undefined && offset > 0 ? offset : 0;
144
+ const windowed = skip > 0 ? records.slice(skip) : records;
102
145
  if (limit === undefined) {
103
- return records;
146
+ return windowed;
104
147
  }
105
148
  if (limit <= 0) {
106
149
  return [];
107
150
  }
108
- return records.length > limit ? records.slice(0, limit) : records;
151
+ return windowed.length > limit ? windowed.slice(0, limit) : windowed;
109
152
  }
110
153
  //# sourceMappingURL=retriever.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, Kind, MemoryId, MemoryScopeKey, Tag } from '../types';\nimport { IIndexedMemoryRecord } from '../index';\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /** Restrict to records of this kind. */\n readonly kind?: Kind;\n /** Restrict to records linked FROM this id (outbound). */\n readonly linkedFrom?: MemoryId;\n /** Restrict to records linked TO this id (inbound / backlinks). */\n readonly linkedTo?: MemoryId;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /** Arbitrary predicate applied after the scope / kind / tag pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag / predicate\n * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /\n * link axes are NOT applied here — those are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {\n return false;\n }\n if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / predicate pre-filter to a set of indexed\n * entries, returning the surviving records (unordered, unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryRecord>,\n query: IMemoryQuery\n): IMemoryRecord<unknown>[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);\n}\n\n/**\n * Truncate to `query.limit` records (a no-op when `limit` is absent). Applied\n * last, after ordering, so it always takes the top-N of the ordered result. A\n * non-positive `limit` is public query input and means \"no records\" — it returns\n * an empty array rather than letting a negative value slip into `slice`.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n if (limit === undefined) {\n return records;\n }\n if (limit <= 0) {\n return [];\n }\n return records.length > limit ? records.slice(0, limit) : records;\n}\n"]}
1
+ {"version":3,"file":"retriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, Tag } from '../types';\nimport { IIndexedMemoryRecord } from '../index';\n\n/**\n * The capabilities a retriever exposes. A consumer probes these before\n * dispatching a query so it never silently gets an empty result for a\n * capability the retriever does not implement.\n * @public\n */\nexport interface IMemoryRetrieverCapabilities {\n /** Semantic / vector recall is operational (an {@link IVectorIndex} is wired). */\n readonly supportsSemanticRecall: boolean;\n /** Temporal \"as-of\" queries are operational (a temporal index is wired). */\n readonly supportsTemporalQuery: boolean;\n /** Link traversal is supported (an in-memory backlink index is present). */\n readonly supportsLinkTraversal: boolean;\n}\n\n/**\n * A retrieval query. Every field is optional; an empty query is the \"recency\n * over everything\" request. `semantic` and `asOf` are present from day one (the\n * no-resignature guarantee): a backend that adds semantic or temporal recall\n * sets the matching capability flag, with no interface change.\n * @public\n */\nexport interface IMemoryQuery {\n /** Restrict to records in this scope. */\n readonly scope?: MemoryScopeKey;\n /** Restrict to records carrying this tag (exact match). */\n readonly tag?: Tag;\n /**\n * Restrict to records of this kind — the single-kind shorthand for\n * {@link IMemoryQuery.kinds | kinds}. When both are set they compose as AND\n * (the record's kind must satisfy both), so `kind` must itself be a member of\n * `kinds` for anything to match.\n */\n readonly kind?: Kind;\n /**\n * Restrict to records in ANY of these kinds — the general (multi-kind) form of\n * {@link IMemoryQuery.kind | kind}. Absent → no kind-set constraint (today's\n * behavior). An explicit empty array `[]` matches NOTHING (mirroring the\n * non-positive-`limit` \"explicit empty\" convention), never \"match all\".\n */\n readonly kinds?: ReadonlyArray<Kind>;\n /** Restrict to records linked FROM this scope-qualified seed (outbound). */\n readonly linkedFrom?: IEdgeTarget;\n /** Restrict to records linked TO this scope-qualified seed (inbound / backlinks). */\n readonly linkedTo?: IEdgeTarget;\n /** BFS hop count for link traversal. Default: 1. */\n readonly hops?: number;\n /**\n * Text query for semantic / vector recall. If set and the retriever's\n * `supportsSemanticRecall` is `false`, the retriever returns a loud\n * `Result.fail` ({@link SEMANTIC_UNWIRED_MESSAGE}) — never a silent empty.\n */\n readonly semantic?: string;\n /** Top-K for semantic recall. Default: 10. */\n readonly topK?: number;\n /**\n * As-of epoch ms for temporal \"valid at\" queries. If set and the retriever's\n * `supportsTemporalQuery` is `false`, the retriever returns a loud\n * `Result.fail` — never a silent empty.\n */\n readonly asOf?: number;\n /**\n * Ordering for the result set. `'recency'` (the default when absent — today's\n * exact behavior) orders most-recently-updated first; `'rank'` orders by the\n * store-computed {@link IMemoryEnvelope.rank} descending (records with an absent\n * `rank` last), with recency as the tiebreak. Combined with `{ limit, offset }`\n * this yields a bounded top-M rank-ordered page with no full-vault scan.\n *\n * @remarks\n * `orderBy` governs the ordered non-semantic retrievers (recency / tag /\n * structured-filter / link-traversal) and the {@link HybridRetriever}'s\n * post-merge ordering. The {@link SemanticRetriever} is the sole exception: it\n * preserves its native vector-similarity order regardless of `orderBy` —\n * re-sorting semantic hits by `rank` would discard the similarity ranking that\n * is the whole point of that path; a consumer that wants rank ordering uses a\n * non-semantic query.\n */\n readonly orderBy?: 'recency' | 'rank';\n /** Maximum records to return. Applied after all other filters. */\n readonly limit?: number;\n /**\n * Records to skip after ordering, before `limit` — so `{ offset, limit }` is a\n * stable page window over the ordered result set. Default 0. A non-positive or\n * absent offset is today's behavior (no skip); an offset past the end yields an\n * empty page, never a throw.\n */\n readonly offset?: number;\n /** Arbitrary predicate applied after the scope / kind / tag pre-filter. */\n readonly filter?: (record: IMemoryRecord<unknown>) => boolean;\n}\n\n/**\n * The retrieval contract. A retriever exposes its {@link\n * IMemoryRetrieverCapabilities | capabilities} and answers\n * {@link IMemoryRetriever.retrieve | queries}, degrading loudly (never silently\n * empty) when a requested capability is not wired.\n * @public\n */\nexport interface IMemoryRetriever {\n /** The capabilities this retriever exposes. Probe before dispatch. */\n readonly capabilities: IMemoryRetrieverCapabilities;\n /**\n * Retrieve records matching `query`. Returns a `Result.fail` with a\n * diagnostic message when the query requests a capability this retriever does\n * not support (never an empty success).\n */\n retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;\n}\n\n/**\n * The loud-degradation message a retriever returns when `query.semantic` is set\n * but no {@link IVectorIndex} is wired.\n * @public\n */\nexport const SEMANTIC_UNWIRED_MESSAGE: string = 'semantic recall requires a vector index; none configured';\n\n/**\n * The loud-degradation message a retriever returns when a link-traversal axis\n * (`linkedFrom` / `linkedTo` / `hops`) is requested but no backlink index is\n * wired.\n * @public\n */\nexport const LINK_TRAVERSAL_UNWIRED_MESSAGE: string =\n 'link traversal requires a backlink index; none configured';\n\n/**\n * The capabilities every non-semantic, non-temporal, non-link v1 retriever\n * exposes (all three flags `false`).\n * @public\n */\nexport const NON_SEMANTIC_CAPABILITIES: IMemoryRetrieverCapabilities = {\n supportsSemanticRecall: false,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n};\n\n/**\n * Build the loud-degradation message a retriever returns when `query.asOf` is\n * set but no temporal index is wired.\n * @public\n */\nexport function temporalUnwiredMessage(kind?: Kind): string {\n return `temporal query requires temporal index; none configured${\n kind !== undefined ? ` for kind ${kind}` : ''\n }`;\n}\n\n/**\n * Enforce the loud-degradation contract for the `semantic` and `asOf` axes\n * against a retriever's `capabilities`: a requested capability the retriever\n * does not support fails loudly rather than returning a silent empty result.\n * @public\n */\nexport function guardRetrieverCapabilities(\n query: IMemoryQuery,\n capabilities: IMemoryRetrieverCapabilities\n): Result<true> {\n if (query.semantic !== undefined && !capabilities.supportsSemanticRecall) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n if (query.asOf !== undefined && !capabilities.supportsTemporalQuery) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n const requestsLinkTraversal: boolean =\n query.linkedFrom !== undefined || query.linkedTo !== undefined || query.hops !== undefined;\n if (requestsLinkTraversal && !capabilities.supportsLinkTraversal) {\n return fail(LINK_TRAVERSAL_UNWIRED_MESSAGE);\n }\n return succeed(true);\n}\n\n/**\n * Recency comparator: most-recently-updated first, with a `seq` tiebreak so\n * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.\n * @public\n */\nexport function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const byUpdated: number = b.envelope.updated - a.envelope.updated;\n return byUpdated !== 0 ? byUpdated : b.envelope.seq - a.envelope.seq;\n}\n\n/**\n * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with\n * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST\n * (after every ranked record), then by recency among themselves. Mirrors the\n * index's rank-view ordering.\n * @public\n */\nexport function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number {\n const ra: number | undefined = a.envelope.rank;\n const rb: number | undefined = b.envelope.rank;\n if (ra === undefined && rb !== undefined) {\n return 1;\n }\n if (rb === undefined && ra !== undefined) {\n return -1;\n }\n if (ra !== undefined && rb !== undefined && ra !== rb) {\n return rb - ra;\n }\n return recencyCompare(a, b);\n}\n\n/**\n * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}\n * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the\n * default, byte-identical to the pre-`orderBy` behavior).\n * @public\n */\nexport function orderingCompare(\n orderBy?: IMemoryQuery['orderBy']\n): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number {\n return orderBy === 'rank' ? rankCompare : recencyCompare;\n}\n\n/**\n * Whether an indexed entry satisfies a query's scope / kind / tag / predicate\n * pre-filter (the axes shared by every v1 retriever). The `semantic` / `asOf` /\n * link axes are NOT applied here — those are each retriever's own concern.\n * @public\n */\nexport function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean {\n if (query.scope !== undefined && entry.scope !== query.scope) {\n return false;\n }\n if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {\n return false;\n }\n if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {\n return false;\n }\n if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {\n return false;\n }\n if (query.filter !== undefined && !query.filter(entry.record)) {\n return false;\n }\n return true;\n}\n\n/**\n * Apply the shared scope / kind / tag / predicate pre-filter to a set of indexed\n * entries, returning the surviving records (unordered, unlimited).\n * @public\n */\nexport function selectByQuery(\n entries: ReadonlyArray<IIndexedMemoryRecord>,\n query: IMemoryQuery\n): IMemoryRecord<unknown>[] {\n return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);\n}\n\n/**\n * Apply the `{ offset, limit }` page window to an ordered record set. Applied\n * last, after ordering, so it always takes a stable window of the ordered\n * result. `offset` is applied first (records to skip), then `limit` (top-N of\n * the remainder).\n *\n * @remarks\n * Both bounds are public query input and are guarded against non-positive\n * values slipping into `slice`:\n * - `offset` absent or non-positive → no skip (today's behavior). An offset past\n * the end yields an empty page rather than a throw.\n * - `limit` absent → no truncation; a non-positive `limit` means \"no records\"\n * and returns an empty array.\n * @public\n */\nexport function limitRecords(\n records: ReadonlyArray<IMemoryRecord<unknown>>,\n limit?: number,\n offset?: number\n): ReadonlyArray<IMemoryRecord<unknown>> {\n const skip: number = offset !== undefined && offset > 0 ? offset : 0;\n const windowed: ReadonlyArray<IMemoryRecord<unknown>> = skip > 0 ? records.slice(skip) : records;\n if (limit === undefined) {\n return windowed;\n }\n if (limit <= 0) {\n return [];\n }\n return windowed.length > limit ? windowed.slice(0, limit) : windowed;\n}\n"]}
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { fail, succeed } from '@fgv/ts-utils';
6
+ import { edgeTargetKey } from '../types';
6
7
  import { SEMANTIC_UNWIRED_MESSAGE, indexedRecordMatchesQuery, limitRecords, temporalUnwiredMessage } from './retriever';
7
8
  /**
8
9
  * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it
@@ -58,15 +59,20 @@ export class SemanticRetriever {
58
59
  if (hits.isFailure()) {
59
60
  return fail(hits.message);
60
61
  }
61
- const byId = new Map(this._index.entries().map((entry) => [entry.record.envelope.id, entry]));
62
+ // Key by the canonical scope-qualified target so a hit re-resolves to the
63
+ // exact record it scored against — a bare id would alias two records that
64
+ // share a filename stem across scopes.
65
+ const byKey = new Map(this._index
66
+ .entries()
67
+ .map((entry) => [edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry]));
62
68
  const records = [];
63
69
  for (const hit of hits.value) {
64
- const entry = byId.get(hit.id);
70
+ const entry = byKey.get(edgeTargetKey(hit.target));
65
71
  if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {
66
72
  records.push(entry.record);
67
73
  }
68
74
  }
69
- return succeed(limitRecords(records, query.limit));
75
+ return succeed(limitRecords(records, query.limit, query.offset));
70
76
  }
71
77
  /**
72
78
  * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`
@@ -1 +1 @@
1
- {"version":3,"file":"semanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, MemoryId } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport { IVectorIndex, IVectorQueryHit } from '../vector';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n SEMANTIC_UNWIRED_MESSAGE,\n indexedRecordMatchesQuery,\n limitRecords,\n temporalUnwiredMessage\n} from './retriever';\n\n/**\n * Embeds a query string into a vector for {@link IVectorIndex.query}. Async and\n * `Result`-returning, since a real embedder does a network call.\n * @public\n */\nexport type QueryEmbedder = (text: string) => Promise<Result<Float32Array>>;\n\n/**\n * The semantic backend wired into a {@link SemanticRetriever}: the vector index\n * to query and the embedder that turns the query text into a vector. Both are\n * required together — a vector index is useless without a way to embed the\n * query, so {@link SemanticRetriever.create} treats them as one unit.\n * @public\n */\nexport interface ISemanticBackend {\n /** The vector index to query. */\n readonly vectorIndex: IVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * Construction options for {@link SemanticRetriever.create}.\n * @public\n */\nexport interface ISemanticRetrieverCreateParams {\n /** The record index, used to resolve vector hits back to full records. */\n readonly index: IMemoryIndex;\n /**\n * The semantic backend. When absent, the retriever reports\n * `supportsSemanticRecall: false` and a `query.semantic` request degrades\n * loudly ({@link SEMANTIC_UNWIRED_MESSAGE}) rather than returning empty.\n */\n readonly backend?: ISemanticBackend;\n}\n\n/**\n * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it\n * embeds `query.semantic`, queries the vector index, and resolves the hits back\n * to records (preserving vector score order). When no backend is wired,\n * `supportsSemanticRecall` is `false` and any `query.semantic` request degrades\n * loudly — it NEVER returns a silent empty.\n *\n * @remarks\n * A consumer-supplied backend that rejects (throws) is normalized into a\n * `Failure` — `retrieve` always honors its `Promise<Result<...>>` contract.\n * @public\n */\nexport class SemanticRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n private readonly _backend: ISemanticBackend | undefined;\n\n private constructor(index: IMemoryIndex, backend: ISemanticBackend | undefined) {\n this._index = index;\n this._backend = backend;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return {\n supportsSemanticRecall: this._backend !== undefined,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n };\n }\n\n /** Family-convention factory. */\n public static create(params: ISemanticRetrieverCreateParams): Result<SemanticRetriever> {\n return succeed(new SemanticRetriever(params.index, params.backend));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n if (query.asOf !== undefined) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n if (query.semantic === undefined) {\n // The semantic term is this retriever's axis; without it there is nothing\n // to recall (a no-op contribution to a HybridRetriever, not a failure).\n return succeed([]);\n }\n if (this._backend === undefined) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: ISemanticBackend = this._backend;\n // The backend hooks are consumer-supplied; a rejecting (throwing) impl must\n // still surface as a `Failure`, never escape `retrieve` as a rejected\n // promise. `_callBackend` normalizes both a returned `fail` and a rejection.\n const embedded: Result<Float32Array> = await SemanticRetriever._callBackend('query embedding', () =>\n backend.embedQuery(query.semantic as string)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n const hits: Result<ReadonlyArray<IVectorQueryHit>> = await SemanticRetriever._callBackend(\n 'vector query',\n () => backend.vectorIndex.query(embedded.value, query.topK ?? 10)\n );\n if (hits.isFailure()) {\n return fail(hits.message);\n }\n const byId: Map<MemoryId, IIndexedMemoryRecord> = new Map(\n this._index.entries().map((entry) => [entry.record.envelope.id, entry])\n );\n const records: IMemoryRecord<unknown>[] = [];\n for (const hit of hits.value) {\n const entry: IIndexedMemoryRecord | undefined = byId.get(hit.id);\n if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {\n records.push(entry.record);\n }\n }\n return succeed(limitRecords(records, query.limit));\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `semantic recall: <label> failed`\n * `Failure`. Keeps `retrieve` within the `Promise<Result<...>>` contract even\n * when the injected `embedQuery` / `vectorIndex` misbehaves.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `semantic recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`semantic recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"semanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/semanticRetriever.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, edgeTargetKey } from '../types';\nimport { IIndexedMemoryRecord, IMemoryIndex } from '../index';\nimport { IVectorIndex, IVectorQueryHit } from '../vector';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n SEMANTIC_UNWIRED_MESSAGE,\n indexedRecordMatchesQuery,\n limitRecords,\n temporalUnwiredMessage\n} from './retriever';\n\n/**\n * Embeds a query string into a vector for {@link IVectorIndex.query}. Async and\n * `Result`-returning, since a real embedder does a network call.\n * @public\n */\nexport type QueryEmbedder = (text: string) => Promise<Result<Float32Array>>;\n\n/**\n * The semantic backend wired into a {@link SemanticRetriever}: the vector index\n * to query and the embedder that turns the query text into a vector. Both are\n * required together — a vector index is useless without a way to embed the\n * query, so {@link SemanticRetriever.create} treats them as one unit.\n * @public\n */\nexport interface ISemanticBackend {\n /** The vector index to query. */\n readonly vectorIndex: IVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * Construction options for {@link SemanticRetriever.create}.\n * @public\n */\nexport interface ISemanticRetrieverCreateParams {\n /** The record index, used to resolve vector hits back to full records. */\n readonly index: IMemoryIndex;\n /**\n * The semantic backend. When absent, the retriever reports\n * `supportsSemanticRecall: false` and a `query.semantic` request degrades\n * loudly ({@link SEMANTIC_UNWIRED_MESSAGE}) rather than returning empty.\n */\n readonly backend?: ISemanticBackend;\n}\n\n/**\n * Vector-recall retriever. When a {@link ISemanticBackend | backend} is wired it\n * embeds `query.semantic`, queries the vector index, and resolves the hits back\n * to records (preserving vector score order). When no backend is wired,\n * `supportsSemanticRecall` is `false` and any `query.semantic` request degrades\n * loudly — it NEVER returns a silent empty.\n *\n * @remarks\n * A consumer-supplied backend that rejects (throws) is normalized into a\n * `Failure` — `retrieve` always honors its `Promise<Result<...>>` contract.\n * @public\n */\nexport class SemanticRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n private readonly _backend: ISemanticBackend | undefined;\n\n private constructor(index: IMemoryIndex, backend: ISemanticBackend | undefined) {\n this._index = index;\n this._backend = backend;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return {\n supportsSemanticRecall: this._backend !== undefined,\n supportsTemporalQuery: false,\n supportsLinkTraversal: false\n };\n }\n\n /** Family-convention factory. */\n public static create(params: ISemanticRetrieverCreateParams): Result<SemanticRetriever> {\n return succeed(new SemanticRetriever(params.index, params.backend));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public async retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n if (query.asOf !== undefined) {\n return fail(temporalUnwiredMessage(query.kind));\n }\n if (query.semantic === undefined) {\n // The semantic term is this retriever's axis; without it there is nothing\n // to recall (a no-op contribution to a HybridRetriever, not a failure).\n return succeed([]);\n }\n if (this._backend === undefined) {\n return fail(SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: ISemanticBackend = this._backend;\n // The backend hooks are consumer-supplied; a rejecting (throwing) impl must\n // still surface as a `Failure`, never escape `retrieve` as a rejected\n // promise. `_callBackend` normalizes both a returned `fail` and a rejection.\n const embedded: Result<Float32Array> = await SemanticRetriever._callBackend('query embedding', () =>\n backend.embedQuery(query.semantic as string)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n const hits: Result<ReadonlyArray<IVectorQueryHit>> = await SemanticRetriever._callBackend(\n 'vector query',\n () => backend.vectorIndex.query(embedded.value, query.topK ?? 10)\n );\n if (hits.isFailure()) {\n return fail(hits.message);\n }\n // Key by the canonical scope-qualified target so a hit re-resolves to the\n // exact record it scored against — a bare id would alias two records that\n // share a filename stem across scopes.\n const byKey: Map<string, IIndexedMemoryRecord> = new Map(\n this._index\n .entries()\n .map((entry) => [edgeTargetKey({ scope: entry.scope, id: entry.record.envelope.id }), entry])\n );\n const records: IMemoryRecord<unknown>[] = [];\n for (const hit of hits.value) {\n const entry: IIndexedMemoryRecord | undefined = byKey.get(edgeTargetKey(hit.target));\n if (entry !== undefined && indexedRecordMatchesQuery(entry, query)) {\n records.push(entry.record);\n }\n }\n return succeed(limitRecords(records, query.limit, query.offset));\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `semantic recall: <label> failed`\n * `Failure`. Keeps `retrieve` within the `Promise<Result<...>>` contract even\n * when the injected `embedQuery` / `vectorIndex` misbehaves.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `semantic recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`semantic recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { succeed } from '@fgv/ts-utils';
6
- import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, recencyCompare, selectByQuery } from './retriever';
6
+ import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
7
7
  /**
8
8
  * Returns records matching `query.filter`, narrowed by any scope / kind / tag
9
9
  * pre-filter and recency-ordered. The predicate is this retriever's axis: a
@@ -29,8 +29,8 @@ export class StructuredFilterRetriever {
29
29
  if (query.filter === undefined) {
30
30
  return succeed([]);
31
31
  }
32
- const ordered = selectByQuery(this._index.entries(), query).sort(recencyCompare);
33
- return succeed(limitRecords(ordered, query.limit));
32
+ const ordered = selectByQuery(this._index.entries(), query).sort(orderingCompare(query.orderBy));
33
+ return succeed(limitRecords(ordered, query.limit, query.offset));
34
34
  }));
35
35
  }
36
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,cAAc,EACd,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,yBAAyB;IAGpC,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC/B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,cAAc,CACf,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n recencyCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching `query.filter`, narrowed by any scope / kind / tag\n * pre-filter and recency-ordered. The predicate is this retriever's axis: a\n * query without a `filter` is not its concern and yields an empty success (so it\n * contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class StructuredFilterRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<StructuredFilterRetriever> {\n return succeed(new StructuredFilterRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.filter === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n recencyCompare\n );\n return succeed(limitRecords(ordered, query.limit));\n })\n );\n }\n}\n"]}
1
+ {"version":3,"file":"structuredFilterRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/structuredFilterRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,yBAAyB;IAGpC,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC/B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records matching `query.filter`, narrowed by any scope / kind / tag\n * pre-filter and recency-ordered. The predicate is this retriever's axis: a\n * query without a `filter` is not its concern and yields an empty success (so it\n * contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class StructuredFilterRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<StructuredFilterRetriever> {\n return succeed(new StructuredFilterRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.filter === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
  import { succeed } from '@fgv/ts-utils';
6
- import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, recencyCompare, selectByQuery } from './retriever';
6
+ import { NON_SEMANTIC_CAPABILITIES, guardRetrieverCapabilities, limitRecords, orderingCompare, selectByQuery } from './retriever';
7
7
  /**
8
8
  * Returns records carrying `query.tag`, recency-ordered within the tag and
9
9
  * narrowed by any scope / kind / predicate filters. Tag is this retriever's
@@ -29,8 +29,8 @@ export class TagRetriever {
29
29
  if (query.tag === undefined) {
30
30
  return succeed([]);
31
31
  }
32
- const ordered = selectByQuery(this._index.entries(), query).sort(recencyCompare);
33
- return succeed(limitRecords(ordered, query.limit));
32
+ const ordered = selectByQuery(this._index.entries(), query).sort(orderingCompare(query.orderBy));
33
+ return succeed(limitRecords(ordered, query.limit, query.offset));
34
34
  }));
35
35
  }
36
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tagRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/tagRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,cAAc,EACd,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IAGvB,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,cAAc,CACf,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n recencyCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records carrying `query.tag`, recency-ordered within the tag and\n * narrowed by any scope / kind / predicate filters. Tag is this retriever's\n * axis: a query without a `tag` is not its concern and yields an empty success\n * (so it contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class TagRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<TagRetriever> {\n return succeed(new TagRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.tag === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n recencyCompare\n );\n return succeed(limitRecords(ordered, query.limit));\n })\n );\n }\n}\n"]}
1
+ {"version":3,"file":"tagRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/tagRetriever.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAIL,yBAAyB,EACzB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IAGvB,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,OAAO,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,0BAA0B,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;YACrB,CAAC;YACD,MAAM,OAAO,GAA6B,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n limitRecords,\n orderingCompare,\n selectByQuery\n} from './retriever';\n\n/**\n * Returns records carrying `query.tag`, recency-ordered within the tag and\n * narrowed by any scope / kind / predicate filters. Tag is this retriever's\n * axis: a query without a `tag` is not its concern and yields an empty success\n * (so it contributes nothing to a {@link HybridRetriever}, rather than failing).\n * @public\n */\nexport class TagRetriever implements IMemoryRetriever {\n private readonly _index: IMemoryIndex;\n\n private constructor(index: IMemoryIndex) {\n this._index = index;\n }\n\n /** {@inheritDoc IMemoryRetriever.capabilities} */\n public get capabilities(): IMemoryRetrieverCapabilities {\n return NON_SEMANTIC_CAPABILITIES;\n }\n\n /** Family-convention factory. */\n public static create(index: IMemoryIndex): Result<TagRetriever> {\n return succeed(new TagRetriever(index));\n }\n\n /** {@inheritDoc IMemoryRetriever.retrieve} */\n public retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {\n return Promise.resolve(\n guardRetrieverCapabilities(query, this.capabilities).onSuccess(() => {\n if (query.tag === undefined) {\n return succeed([]);\n }\n const ordered: IMemoryRecord<unknown>[] = selectByQuery(this._index.entries(), query).sort(\n orderingCompare(query.orderBy)\n );\n return succeed(limitRecords(ordered, query.limit, query.offset));\n })\n );\n }\n}\n"]}