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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/packlets/index/memoryIndex.js +23 -16
  2. package/dist/packlets/index/memoryIndex.js.map +1 -1
  3. package/dist/packlets/ingest/orchestrator.js +13 -1
  4. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  5. package/dist/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  6. package/dist/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  7. package/dist/packlets/retrieve/linkTraversalRetriever.js +12 -26
  8. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  9. package/dist/packlets/retrieve/recencyRetriever.js +7 -7
  10. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  11. package/dist/packlets/retrieve/retriever.js +91 -10
  12. package/dist/packlets/retrieve/retriever.js.map +1 -1
  13. package/dist/packlets/retrieve/semanticRetriever.js +16 -16
  14. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  15. package/dist/packlets/retrieve/structuredFilterRetriever.js +7 -7
  16. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  17. package/dist/packlets/retrieve/tagRetriever.js +7 -7
  18. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  19. package/dist/packlets/retrieve/temporalRetrievers.js +23 -20
  20. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  21. package/dist/packlets/store/coverage.js +6 -0
  22. package/dist/packlets/store/coverage.js.map +1 -0
  23. package/dist/packlets/store/fileTreeMemoryStore.js +234 -103
  24. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  25. package/dist/packlets/store/index.js +4 -0
  26. package/dist/packlets/store/index.js.map +1 -1
  27. package/dist/packlets/store/listSelection.js +36 -0
  28. package/dist/packlets/store/listSelection.js.map +1 -0
  29. package/dist/packlets/store/memoryStore.js +6 -0
  30. package/dist/packlets/store/memoryStore.js.map +1 -0
  31. package/dist/packlets/store/reconcile.js +6 -0
  32. package/dist/packlets/store/reconcile.js.map +1 -0
  33. package/dist/packlets/store/storeCoverage.js +102 -0
  34. package/dist/packlets/store/storeCoverage.js.map +1 -0
  35. package/dist/packlets/store/storeIdentity.js +62 -0
  36. package/dist/packlets/store/storeIdentity.js.map +1 -0
  37. package/dist/packlets/store/storeReconcile.js +122 -0
  38. package/dist/packlets/store/storeReconcile.js.map +1 -0
  39. package/dist/packlets/store/vectorMaintenance.js +116 -8
  40. package/dist/packlets/store/vectorMaintenance.js.map +1 -1
  41. package/dist/packlets/store/vectorRecordSource.js +44 -0
  42. package/dist/packlets/store/vectorRecordSource.js.map +1 -0
  43. package/dist/packlets/tools/memoryTools.js +25 -2
  44. package/dist/packlets/tools/memoryTools.js.map +1 -1
  45. package/dist/packlets/types/envelope.js +25 -0
  46. package/dist/packlets/types/envelope.js.map +1 -1
  47. package/dist/packlets/types/identityResolver.js +6 -0
  48. package/dist/packlets/types/identityResolver.js.map +1 -0
  49. package/dist/packlets/types/index.js +2 -0
  50. package/dist/packlets/types/index.js.map +1 -1
  51. package/dist/packlets/types/recordResolver.js +6 -0
  52. package/dist/packlets/types/recordResolver.js.map +1 -0
  53. package/dist/packlets/types/temporal.js.map +1 -1
  54. package/dist/packlets/vector/inMemoryCosineIndex.js +39 -18
  55. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  56. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +109 -14
  57. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  58. package/dist/packlets/vector/rebuildHelpers.js +38 -0
  59. package/dist/packlets/vector/rebuildHelpers.js.map +1 -0
  60. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  61. package/dist/ts-agent-memory.d.ts +1226 -114
  62. package/lib/packlets/index/memoryIndex.d.ts +118 -27
  63. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  64. package/lib/packlets/index/memoryIndex.js +23 -16
  65. package/lib/packlets/index/memoryIndex.js.map +1 -1
  66. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  67. package/lib/packlets/ingest/orchestrator.js +13 -1
  68. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  69. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts +79 -1
  70. package/lib/packlets/retrieve/fragmentSemanticRetriever.d.ts.map +1 -1
  71. package/lib/packlets/retrieve/fragmentSemanticRetriever.js +94 -5
  72. package/lib/packlets/retrieve/fragmentSemanticRetriever.js.map +1 -1
  73. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +3 -10
  74. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  75. package/lib/packlets/retrieve/linkTraversalRetriever.js +11 -25
  76. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  77. package/lib/packlets/retrieve/recencyRetriever.d.ts +3 -3
  78. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -1
  79. package/lib/packlets/retrieve/recencyRetriever.js +6 -6
  80. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  81. package/lib/packlets/retrieve/retriever.d.ts +88 -7
  82. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  83. package/lib/packlets/retrieve/retriever.js +94 -9
  84. package/lib/packlets/retrieve/retriever.js.map +1 -1
  85. package/lib/packlets/retrieve/semanticRetriever.d.ts +3 -5
  86. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  87. package/lib/packlets/retrieve/semanticRetriever.js +15 -15
  88. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  89. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +3 -3
  90. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  91. package/lib/packlets/retrieve/structuredFilterRetriever.js +6 -6
  92. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  93. package/lib/packlets/retrieve/tagRetriever.d.ts +3 -3
  94. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -1
  95. package/lib/packlets/retrieve/tagRetriever.js +6 -6
  96. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  97. package/lib/packlets/retrieve/temporalRetrievers.d.ts +7 -5
  98. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -1
  99. package/lib/packlets/retrieve/temporalRetrievers.js +22 -19
  100. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  101. package/lib/packlets/store/coverage.d.ts +102 -0
  102. package/lib/packlets/store/coverage.d.ts.map +1 -0
  103. package/lib/packlets/store/coverage.js +7 -0
  104. package/lib/packlets/store/coverage.js.map +1 -0
  105. package/lib/packlets/store/fileTreeMemoryStore.d.ts +56 -167
  106. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  107. package/lib/packlets/store/fileTreeMemoryStore.js +234 -103
  108. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  109. package/lib/packlets/store/index.d.ts +4 -0
  110. package/lib/packlets/store/index.d.ts.map +1 -1
  111. package/lib/packlets/store/index.js +4 -0
  112. package/lib/packlets/store/index.js.map +1 -1
  113. package/lib/packlets/store/listSelection.d.ts +101 -0
  114. package/lib/packlets/store/listSelection.d.ts.map +1 -0
  115. package/lib/packlets/store/listSelection.js +40 -0
  116. package/lib/packlets/store/listSelection.js.map +1 -0
  117. package/lib/packlets/store/memoryStore.d.ts +237 -0
  118. package/lib/packlets/store/memoryStore.d.ts.map +1 -0
  119. package/lib/packlets/store/memoryStore.js +7 -0
  120. package/lib/packlets/store/memoryStore.js.map +1 -0
  121. package/lib/packlets/store/reconcile.d.ts +82 -0
  122. package/lib/packlets/store/reconcile.d.ts.map +1 -0
  123. package/lib/packlets/store/reconcile.js +7 -0
  124. package/lib/packlets/store/reconcile.js.map +1 -0
  125. package/lib/packlets/store/storeCoverage.d.ts +45 -0
  126. package/lib/packlets/store/storeCoverage.d.ts.map +1 -0
  127. package/lib/packlets/store/storeCoverage.js +105 -0
  128. package/lib/packlets/store/storeCoverage.js.map +1 -0
  129. package/lib/packlets/store/storeIdentity.d.ts +38 -0
  130. package/lib/packlets/store/storeIdentity.d.ts.map +1 -0
  131. package/lib/packlets/store/storeIdentity.js +67 -0
  132. package/lib/packlets/store/storeIdentity.js.map +1 -0
  133. package/lib/packlets/store/storeReconcile.d.ts +41 -0
  134. package/lib/packlets/store/storeReconcile.d.ts.map +1 -0
  135. package/lib/packlets/store/storeReconcile.js +125 -0
  136. package/lib/packlets/store/storeReconcile.js.map +1 -0
  137. package/lib/packlets/store/vectorMaintenance.d.ts +74 -0
  138. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -1
  139. package/lib/packlets/store/vectorMaintenance.js +117 -8
  140. package/lib/packlets/store/vectorMaintenance.js.map +1 -1
  141. package/lib/packlets/store/vectorRecordSource.d.ts +36 -0
  142. package/lib/packlets/store/vectorRecordSource.d.ts.map +1 -0
  143. package/lib/packlets/store/vectorRecordSource.js +47 -0
  144. package/lib/packlets/store/vectorRecordSource.js.map +1 -0
  145. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  146. package/lib/packlets/tools/memoryTools.js +25 -2
  147. package/lib/packlets/tools/memoryTools.js.map +1 -1
  148. package/lib/packlets/types/envelope.d.ts +24 -2
  149. package/lib/packlets/types/envelope.d.ts.map +1 -1
  150. package/lib/packlets/types/envelope.js +26 -0
  151. package/lib/packlets/types/envelope.js.map +1 -1
  152. package/lib/packlets/types/identityResolver.d.ts +42 -0
  153. package/lib/packlets/types/identityResolver.d.ts.map +1 -0
  154. package/lib/packlets/types/identityResolver.js +7 -0
  155. package/lib/packlets/types/identityResolver.js.map +1 -0
  156. package/lib/packlets/types/index.d.ts +2 -0
  157. package/lib/packlets/types/index.d.ts.map +1 -1
  158. package/lib/packlets/types/index.js +2 -0
  159. package/lib/packlets/types/index.js.map +1 -1
  160. package/lib/packlets/types/recordResolver.d.ts +39 -0
  161. package/lib/packlets/types/recordResolver.d.ts.map +1 -0
  162. package/lib/packlets/types/recordResolver.js +7 -0
  163. package/lib/packlets/types/recordResolver.js.map +1 -0
  164. package/lib/packlets/types/temporal.d.ts +26 -6
  165. package/lib/packlets/types/temporal.d.ts.map +1 -1
  166. package/lib/packlets/types/temporal.js.map +1 -1
  167. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -2
  168. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  169. package/lib/packlets/vector/inMemoryCosineIndex.js +40 -19
  170. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  171. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +18 -4
  172. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  173. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +108 -13
  174. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  175. package/lib/packlets/vector/rebuildHelpers.d.ts +30 -0
  176. package/lib/packlets/vector/rebuildHelpers.d.ts.map +1 -0
  177. package/lib/packlets/vector/rebuildHelpers.js +42 -0
  178. package/lib/packlets/vector/rebuildHelpers.js.map +1 -0
  179. package/lib/packlets/vector/vectorIndex.d.ts +326 -20
  180. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  181. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  182. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAsD;AAItD;;;;;GAKG;AACU,QAAA,iCAAiC,GAC5C,6GAA6G,CAAC;AA0ChH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,yBAAyB;IAGpC,YAAoB,OAA6C;QAC/D,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,mDAAmD;IACnD,IAAW,YAAY;QACrB,OAAO,EAAE,sBAAsB,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;IACjE,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,MAEpB;QACC,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,KAAqB;QACzC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,IAAA,eAAI,EAAC,yCAAiC,CAAC,CAAC;QACjD,CAAC;QACD,MAAM,OAAO,GAA6B,IAAI,CAAC,QAAQ,CAAC;QACxD,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,QAAQ,GAAyB,MAAM,yBAAyB,CAAC,YAAY,CACjF,iBAAiB,EACjB,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CACzC,CAAC;QACF,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YACzB,OAAO,IAAA,eAAI,EAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,yBAAyB,CAAC,YAAY,CAAC,gBAAgB,EAAE,GAAG,EAAE,WACnE,OAAA,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAA,KAAK,CAAC,IAAI,mCAAI,EAAE,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA,EAAA,CAClF,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAK,CAAC,YAAY,CAAI,KAAa,EAAE,EAA4B;QAC9E,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,KAAK,YAAY,GAAG,EAAE,CAAC,CAAC;QAC3F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAA,eAAI,EAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAvDD,8DAuDC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n\n private constructor(backend: IFragmentSemanticBackend | undefined) {\n this._backend = backend;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /** Family-convention factory. */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, query.maxPerRecord)\n );\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"fragmentSemanticRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/fragmentSemanticRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAqE;AACrE,oCAAsG;AAItG;;;;;GAKG;AACU,QAAA,iCAAiC,GAC5C,6GAA6G,CAAC;AAEhH;;;;;;;;GAQG;AACU,QAAA,uCAAuC,GAClD,kIAAkI,CAAC;AAErI;;;;;;;;GAQG;AACU,QAAA,qCAAqC,GAChD,kIAAkI,CAAC;AA2ErI;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,yBAAyB;IAIpC,YACE,OAA6C,EAC7C,gBAA+C;QAE/C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,mDAAmD;IACnD,IAAW,YAAY;QACrB,OAAO,EAAE,sBAAsB,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;IACjE,CAAC;IAED;;;;;;;;;OASG;IACI,MAAM,CAAC,MAAM,CAAC,MAGpB;QACC,OAAO,IAAA,kBAAO,EAAC,IAAI,yBAAyB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACzF,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,KAAqB;QACzC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,IAAA,eAAI,EAAC,yCAAiC,CAAC,CAAC;QACjD,CAAC;QACD,MAAM,OAAO,GAA6B,IAAI,CAAC,QAAQ,CAAC;QACxD,0EAA0E;QAC1E,8EAA8E;QAC9E,sEAAsE;QACtE,MAAM,OAAO,GAAkC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC3E,IAAI,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;YACxB,OAAO,IAAA,eAAI,EAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;QACD,4EAA4E;QAC5E,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,QAAQ,GAAyB,MAAM,yBAAyB,CAAC,YAAY,CACjF,iBAAiB,EACjB,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CACzC,CAAC;QACF,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YACzB,OAAO,IAAA,eAAI,EAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,yBAAyB,CAAC,YAAY,CAAC,gBAAgB,EAAE,GAAG,EAAE,WACnE,OAAA,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAA,KAAK,CAAC,IAAI,mCAAI,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA,EAAA,CAC7E,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,eAAe,CAAC,KAAqB;QAC3C,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;QAC/C,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACjD,OAAO,IAAA,kBAAO,EAAC,EAAE,YAAY,EAAE,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACjD,OAAO,IAAA,eAAI,EAAC,6CAAqC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,IAAI,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACzC,OAAO,IAAA,eAAI,EAAC,+CAAuC,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,QAAQ,GAAsB,IAAI,CAAC,iBAAiB,CAAC;QAC3D,+EAA+E;QAC/E,8EAA8E;QAC9E,6EAA6E;QAC7E,4DAA4D;QAC5D,OAAO,IAAA,wBAAa,EAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;aACjE,SAAS,CAAC,CAAC,QAAsC,EAAE,EAAE,CAAC,QAAQ,CAAC;aAC/D,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oCAAoC,IAAI,MAAM,QAAQ,MAAM,GAAG,EAAE,CAAC;aAC3F,SAAS,CAAC,CAAC,OAA6B,EAAE,EAAE;YAC3C,sEAAsE;YACtE,oEAAoE;YACpE,mDAAmD;YACnD,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBACxB,OAAO,IAAA,kBAAO,EAAC,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YACzD,CAAC;YACD,4EAA4E;YAC5E,2EAA2E;YAC3E,yEAAyE;YACzE,uEAAuE;YACvE,wEAAwE;YACxE,OAAO,eAAO,CAAC,QAAQ;iBACpB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;iBACvB,eAAe,CACd,CAAC,GAAG,EAAE,EAAE,CAAC,qBAAqB,IAAI,MAAM,QAAQ,wCAAwC,GAAG,EAAE,CAC9F;iBACA,SAAS,CAAC,CAAC,EAAY,EAAE,EAAE,CAAC,IAAA,kBAAO,EAAC,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACtF,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAK,CAAC,YAAY,CAAI,KAAa,EAAE,EAA4B;QAC9E,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,KAAK,YAAY,GAAG,EAAE,CAAC,CAAC;QAC3F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,IAAA,eAAI,EAAC,oBAAoB,KAAK,YAAY,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAnID,8DAmIC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, captureResult, fail, succeed } from '@fgv/ts-utils';\nimport { Convert, EntityId, IIdentityCodecResult, IIdentityResolver, Kind, MemoryId } from '../types';\nimport { IFragmentQueryOptions, IFragmentVectorIndex, IVectorQueryHit } from '../vector';\nimport { QueryEmbedder } from './semanticRetriever';\n\n/**\n * The loud-degradation message a {@link FragmentSemanticRetriever} returns when a\n * fragment query is issued but no {@link IFragmentSemanticBackend | backend} is\n * wired — the discovery surface NEVER answers a fragment query with a silent empty.\n * @public\n */\nexport const FRAGMENT_SEMANTIC_UNWIRED_MESSAGE: string =\n 'fragment recall: no fragment index is wired; wire an IFragmentSemanticBackend to enable sub-document search';\n\n/**\n * The loud-degradation message returned when a query carries a record narrowing but\n * no {@link IIdentityResolver} is wired to resolve it.\n *\n * @remarks\n * Deliberately a `Failure` rather than a silently-global search: answering a scoped\n * question with an unscoped result is the failure this narrowing exists to remove.\n * @public\n */\nexport const FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE: string =\n 'fragment recall: a record narrowing was supplied but no identity resolver is wired; pass one to FragmentSemanticRetriever.create';\n\n/**\n * The message returned when exactly one of `entityId` / `kind` is supplied.\n *\n * @remarks\n * They travel together because `kind` is what selects the identity codec, and the\n * codec is what makes the resolution unambiguous. One without the other is not a\n * partial narrowing that could be honored best-effort — it is not a narrowing at all.\n * @public\n */\nexport const FRAGMENT_NARROWING_INCOMPLETE_MESSAGE: string =\n 'fragment recall: `entityId` and `kind` must be supplied together — `kind` selects the identity codec that resolves the narrowing';\n\n/**\n * The fragment backend wired into a {@link FragmentSemanticRetriever}: the fragment\n * index to query and the embedder that turns the query text into a vector. Both are\n * required together — a fragment index is useless without a way to embed the query.\n * @public\n */\nexport interface IFragmentSemanticBackend {\n /** The fragment-granular vector index to query. */\n readonly fragmentIndex: IFragmentVectorIndex;\n /** Turns the query text into a vector. */\n readonly embedQuery: QueryEmbedder;\n}\n\n/**\n * A sub-document semantic-search request: the natural-language `semantic` text to\n * match, an optional `topK` result cap (default 10), and an optional\n * `maxPerRecord` cap that keeps one long document from monopolizing the result.\n * @public\n */\nexport interface IFragmentQuery {\n /** The natural-language text to embed and match against stored fragments. */\n readonly semantic: string;\n /** Maximum number of fragment hits to return. Defaults to 10. */\n readonly topK?: number;\n /**\n * Maximum number of fragments any single record may contribute to the result.\n * Applied during selection (before the `topK` cut). Omit for uncapped.\n */\n readonly maxPerRecord?: number;\n\n /**\n * Narrow the search to one record's fragments: the consumer-supplied domain key\n * of the record to search within. **Must be supplied with\n * {@link IFragmentQuery.kind}.**\n *\n * @remarks\n * The narrowing is applied **during selection, before the `topK` cut**, so the\n * `topK` you ask for is the `topK` you get. Filtering a global result afterwards\n * is not equivalent: it truncates to `topK` across every record first, so a scoped\n * search would come back short whenever other records outscored this one's\n * fragments.\n *\n * For a versioned kind this narrows to **every version of the entity** — literally\n * every version, superseded ones included, because invalidation stamps `invalid_at`\n * without pruning that version's fragments. Nothing on a hit distinguishes a\n * current fragment from a historical one. That matches the record-granular vector\n * lane; it is not currency filtering.\n */\n readonly entityId?: EntityId;\n\n /**\n * The kind of the record named by {@link IFragmentQuery.entityId}. **Must be\n * supplied with it.**\n *\n * @remarks\n * This is not decoration and not a filter: `kind` **selects the identity codec**,\n * and the codec computes the storage address. An `EntityId` promises no uniqueness\n * beyond a scope — the same id under two kinds is the ordinary case, not a\n * pathological one — so without `kind` the resolution is ambiguous, and with it\n * ambiguity is structurally impossible.\n */\n readonly kind?: Kind;\n}\n\n/**\n * What a {@link FragmentSemanticRetriever} can do given its wiring.\n * @public\n */\nexport interface IFragmentRetrieverCapabilities {\n /** `true` when a fragment backend is wired and fragment recall is operational. */\n readonly supportsFragmentRecall: boolean;\n}\n\n/**\n * The sub-document semantic-search retriever — the \"discovery\" half of a\n * search-then-read contract. It embeds a fragment query, queries the\n * {@link IFragmentVectorIndex}, and returns the raw per-fragment\n * {@link IVectorQueryHit | hits} (each carrying a record `target` AND whichever of\n * `locator` / `fragmentId` the stored fragment was indexed with), NOT resolved\n * records: the consumer re-reads each record and resolves the fragment on its own\n * read side. Note the `locator` span is advisory — see {@link IFragmentLocator}; it\n * is not a slice guaranteed to reproduce the fragment's text.\n *\n * @remarks\n * Deliberately NOT an {@link IMemoryRetriever}: memory recall is record-granular and\n * returns records; fragment discovery is fragment-granular and returns fragment\n * identities. Keeping it a distinct surface matches the consumer contract (memory\n * stays record-granular; sub-document knowledge uses a separate fragment index) and\n * avoids overloading the record retriever's return type with identity fields that\n * only make sense here.\n *\n * When no backend is wired, `supportsFragmentRecall` is `false` and any fragment\n * query degrades loudly ({@link FRAGMENT_SEMANTIC_UNWIRED_MESSAGE}) — it NEVER\n * returns a silent empty. A consumer-supplied backend that rejects (throws) is\n * normalized into a `Failure`.\n * @public\n */\nexport class FragmentSemanticRetriever {\n private readonly _backend: IFragmentSemanticBackend | undefined;\n private readonly _identityResolver: IIdentityResolver | undefined;\n\n private constructor(\n backend: IFragmentSemanticBackend | undefined,\n identityResolver: IIdentityResolver | undefined\n ) {\n this._backend = backend;\n this._identityResolver = identityResolver;\n }\n\n /** What this retriever can do given its wiring. */\n public get capabilities(): IFragmentRetrieverCapabilities {\n return { supportsFragmentRecall: this._backend !== undefined };\n }\n\n /**\n * Family-convention factory.\n *\n * @param params - `backend` wires fragment recall itself. `identityResolver`\n * resolves a query's `(kind, entityId)` narrowing to a storage address;\n * `IMemoryStore` implements it, so the usual wiring is\n * `{ backend, identityResolver: store }`. It is optional because an unscoped\n * fragment search needs nothing to resolve — but a query that *does* carry a\n * narrowing fails loudly without it rather than quietly searching everything.\n */\n public static create(params: {\n readonly backend?: IFragmentSemanticBackend;\n readonly identityResolver?: IIdentityResolver;\n }): Result<FragmentSemanticRetriever> {\n return succeed(new FragmentSemanticRetriever(params.backend, params.identityResolver));\n }\n\n /**\n * Embed `query.semantic`, query the fragment index, and return the per-fragment\n * hits in descending score order. Fails loudly when no backend is wired.\n */\n public async retrieve(query: IFragmentQuery): Promise<Result<ReadonlyArray<IVectorQueryHit>>> {\n if (this._backend === undefined) {\n return fail(FRAGMENT_SEMANTIC_UNWIRED_MESSAGE);\n }\n const backend: IFragmentSemanticBackend = this._backend;\n // Resolve the narrowing FIRST. It is synchronous, local, and cheap, while\n // `embedQuery` is typically a paid network round trip — so a typo'd `kind`, a\n // missing resolver, or a half-supplied narrowing should cost nothing.\n const options: Result<IFragmentQueryOptions> = this._resolveOptions(query);\n if (options.isFailure()) {\n return fail(options.message);\n }\n // Consumer-supplied hooks may throw; normalize both a returned `fail` and a\n // rejection into a single `fragment recall: <label> failed` Failure so\n // `retrieve` always honors its `Promise<Result<...>>` contract.\n const embedded: Result<Float32Array> = await FragmentSemanticRetriever._callBackend(\n 'query embedding',\n () => backend.embedQuery(query.semantic)\n );\n if (embedded.isFailure()) {\n return fail(embedded.message);\n }\n return FragmentSemanticRetriever._callBackend('fragment query', () =>\n backend.fragmentIndex.query(embedded.value, query.topK ?? 10, options.value)\n );\n }\n\n /**\n * Turn the query's consumer-facing narrowing into the storage-address narrowing\n * the index understands.\n *\n * @remarks\n * `kind` selects the identity codec and the codec computes the record's storage\n * address, so this is a deterministic resolution rather than a search —\n * which is what makes a colliding `entityId` across kinds a non-issue.\n *\n * A **versioned** kind resolves to the entity's own subtree scope and deliberately\n * carries no `id`, so the narrowing covers every version of the entity — including\n * superseded ones, which are invalidated but never pruned from the index. A\n * non-versioned kind resolves to exactly one record.\n */\n private _resolveOptions(query: IFragmentQuery): Result<IFragmentQueryOptions> {\n const { entityId, kind, maxPerRecord } = query;\n if (entityId === undefined && kind === undefined) {\n return succeed({ maxPerRecord });\n }\n if (entityId === undefined || kind === undefined) {\n return fail(FRAGMENT_NARROWING_INCOMPLETE_MESSAGE);\n }\n if (this._identityResolver === undefined) {\n return fail(FRAGMENT_NARROWING_UNRESOLVABLE_MESSAGE);\n }\n const resolver: IIdentityResolver = this._identityResolver;\n // `identityResolver` is a consumer-injectable seam like the two backend hooks,\n // so a throw has to become a `Failure` here rather than escaping `retrieve()`\n // and breaking its `Promise<Result<...>>` contract. `captureResult` yields a\n // nested `Result`, which the identity `onSuccess` flattens.\n return captureResult(() => resolver.resolveIdentity(kind, entityId))\n .onSuccess((resolved: Result<IIdentityCodecResult>) => resolved)\n .withErrorFormat((msg) => `fragment recall: cannot resolve '${kind}'/'${entityId}': ${msg}`)\n .onSuccess((address: IIdentityCodecResult) => {\n // A versioned kind's every version lives under the entity subtree the\n // codec returned, so omitting `id` is what makes the narrowing mean\n // \"this entity\" rather than \"one of its versions\".\n if (address.isVersioned) {\n return succeed({ maxPerRecord, scope: address.scope });\n }\n // `idStem` is a plain `string` on the codec result, but a `MemoryId` IS the\n // filename stem by contract and `Convert.memoryId` is what enforces that —\n // so validate rather than assert. A resolver that returned a path-unsafe\n // stem could otherwise smuggle it into the index query, where it would\n // match nothing and look like an empty result rather than a caller bug.\n return Convert.memoryId\n .convert(address.idStem)\n .withErrorFormat(\n (msg) => `fragment recall: '${kind}'/'${entityId}' resolved to an unusable record id: ${msg}`\n )\n .onSuccess((id: MemoryId) => succeed({ maxPerRecord, scope: address.scope, id }));\n });\n }\n\n /**\n * Invoke a consumer-supplied backend hook, normalizing both a returned `fail`\n * and a thrown/rejected promise into a single `fragment recall: <label> failed`\n * `Failure`.\n */\n private static async _callBackend<T>(label: string, op: () => Promise<Result<T>>): Promise<Result<T>> {\n try {\n return (await op()).withErrorFormat((msg) => `fragment recall: ${label} failed: ${msg}`);\n } catch (err) {\n return fail(`fragment recall: ${label} failed: ${String(err)}`);\n }\n }\n}\n"]}
@@ -1,7 +1,6 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
2
  import { IMemoryRecord } from '../types';
3
- import { IMemoryIndex } from '../index';
4
- import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './retriever';
3
+ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities, IRetrieverCreateParams } from './retriever';
5
4
  /**
6
5
  * The loud-degradation message returned when a link-traversal query supplies no
7
6
  * seed (`linkedFrom` / `linkedTo`).
@@ -32,22 +31,16 @@ export declare const LINK_TRAVERSAL_NO_SEED_MESSAGE: string;
32
31
  */
33
32
  export declare class LinkTraversalRetriever implements IMemoryRetriever {
34
33
  private readonly _index;
34
+ private readonly _resolver;
35
35
  private constructor();
36
36
  /** Family-convention factory. */
37
- static create(index: IMemoryIndex): Result<LinkTraversalRetriever>;
37
+ static create(params: IRetrieverCreateParams): Result<LinkTraversalRetriever>;
38
38
  /** {@inheritDoc IMemoryRetriever.capabilities} */
39
39
  get capabilities(): IMemoryRetrieverCapabilities;
40
40
  /** {@inheritDoc IMemoryRetriever.retrieve} */
41
41
  retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
42
42
  /** Run the bounded, cycle-safe BFS and post-filter the reached records. */
43
43
  private _traverse;
44
- /**
45
- * Group the index's entries by their scope-qualified {@link edgeTargetKey}
46
- * `(scope, id)` composite. Each composite is the index's primary key, so it maps
47
- * to exactly one entry — two records that reuse a filename stem across scopes
48
- * (e.g. `turn-0` in two conversations) get distinct keys and never collide.
49
- */
50
- private _indexByKey;
51
44
  /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */
52
45
  private _outbound;
53
46
  /** Inbound neighbors: the scope-qualified sources whose edges point AT `node` (the backlinks). */
@@ -1 +1 @@
1
- {"version":3,"file":"linkTraversalRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAe,aAAa,EAAiB,MAAM,UAAU,CAAC;AACrE,OAAO,EAAwB,YAAY,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAK7B,MAAM,aAAa,CAAC;AAYrB;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,MACiB,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO;IAIP,iCAAiC;WACnB,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,sBAAsB,CAAC;IAIzE,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAM5F,2EAA2E;IAC3E,OAAO,CAAC,SAAS;IA4CjB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAQnB,6FAA6F;IAC7F,OAAO,CAAC,SAAS;IAWjB,kGAAkG;IAClG,OAAO,CAAC,QAAQ;CAGjB"}
1
+ {"version":3,"file":"linkTraversalRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/linkTraversalRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtD,OAAO,EAAe,aAAa,EAAwC,MAAM,UAAU,CAAC;AAE5F,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAC5B,sBAAsB,EAKvB,MAAM,aAAa,CAAC;AAYrB;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,MACiB,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,OAAO;IAKP,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,CAAC;IAIpF,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAM5F,2EAA2E;IAC3E,OAAO,CAAC,SAAS;IA0CjB,6FAA6F;IAC7F,OAAO,CAAC,SAAS;IAWjB,kGAAkG;IAClG,OAAO,CAAC,QAAQ;CAGjB"}
@@ -45,12 +45,13 @@ exports.LINK_TRAVERSAL_NO_SEED_MESSAGE = 'link traversal requires a seed id (lin
45
45
  * @public
46
46
  */
47
47
  class LinkTraversalRetriever {
48
- constructor(index) {
49
- this._index = index;
48
+ constructor(params) {
49
+ this._index = params.index;
50
+ this._resolver = params.resolver;
50
51
  }
51
52
  /** Family-convention factory. */
52
- static create(index) {
53
- return (0, ts_utils_1.succeed)(new LinkTraversalRetriever(index));
53
+ static create(params) {
54
+ return (0, ts_utils_1.succeed)(new LinkTraversalRetriever(params));
54
55
  }
55
56
  /** {@inheritDoc IMemoryRetriever.capabilities} */
56
57
  get capabilities() {
@@ -69,7 +70,6 @@ class LinkTraversalRetriever {
69
70
  return (0, ts_utils_1.fail)(exports.LINK_TRAVERSAL_NO_SEED_MESSAGE);
70
71
  }
71
72
  const hops = (_b = query.hops) !== null && _b !== void 0 ? _b : DEFAULT_HOPS;
72
- const byKey = this._indexByKey();
73
73
  // The visited-set IS the cycle guard: nodes are canonicalized to their
74
74
  // `(scope, id)` string, so set membership is an exact identity check. The
75
75
  // seed is pre-marked so it is never re-added.
@@ -79,7 +79,7 @@ class LinkTraversalRetriever {
79
79
  for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
80
80
  const next = [];
81
81
  for (const node of frontier) {
82
- for (const neighbor of outbound ? this._outbound(node, byKey) : this._inbound(node)) {
82
+ for (const neighbor of outbound ? this._outbound(node) : this._inbound(node)) {
83
83
  const neighborKey = (0, types_1.edgeTargetKey)(neighbor);
84
84
  if (!visited.has(neighborKey)) {
85
85
  visited.add(neighborKey);
@@ -92,36 +92,22 @@ class LinkTraversalRetriever {
92
92
  }
93
93
  const entries = [];
94
94
  for (const node of reached) {
95
- const match = byKey.get((0, types_1.edgeTargetKey)(node));
95
+ const match = this._index.get(node);
96
96
  if (match !== undefined) {
97
97
  entries.push(match);
98
98
  }
99
99
  }
100
100
  const ordered = entries
101
101
  .filter((entry) => (0, retriever_1.indexedRecordMatchesQuery)(entry, query))
102
- .map((entry) => entry.record)
103
102
  .sort((0, retriever_1.orderingCompare)(query.orderBy));
104
- return (0, ts_utils_1.succeed)((0, retriever_1.limitRecords)(ordered, query.limit, query.offset));
105
- }
106
- /**
107
- * Group the index's entries by their scope-qualified {@link edgeTargetKey}
108
- * `(scope, id)` composite. Each composite is the index's primary key, so it maps
109
- * to exactly one entry — two records that reuse a filename stem across scopes
110
- * (e.g. `turn-0` in two conversations) get distinct keys and never collide.
111
- */
112
- _indexByKey() {
113
- const byKey = new Map();
114
- for (const entry of this._index.entries()) {
115
- byKey.set((0, types_1.edgeTargetKey)({ scope: entry.scope, id: entry.record.envelope.id }), entry);
116
- }
117
- return byKey;
103
+ return (0, retriever_1.materializePage)(ordered, query, this._resolver);
118
104
  }
119
105
  /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */
120
- _outbound(node, byKey) {
106
+ _outbound(node) {
121
107
  const targets = [];
122
- const match = byKey.get((0, types_1.edgeTargetKey)(node));
108
+ const match = this._index.get(node);
123
109
  if (match !== undefined) {
124
- for (const edge of match.record.envelope.links) {
110
+ for (const edge of match.envelope.links) {
125
111
  targets.push(edge.target);
126
112
  }
127
113
  }
@@ -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 { 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 / provenance-source / 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"]}
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, IMemoryRecordResolver, edgeTargetKey } from '../types';\nimport { IIndexedMemoryEntry, IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n IRetrieverCreateParams,\n guardRetrieverCapabilities,\n indexedRecordMatchesQuery,\n materializePage,\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 / provenance-source / 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 private readonly _resolver: IMemoryRecordResolver;\n\n private constructor(params: IRetrieverCreateParams) {\n this._index = params.index;\n this._resolver = params.resolver;\n }\n\n /** Family-convention factory. */\n public static create(params: IRetrieverCreateParams): Result<LinkTraversalRetriever> {\n return succeed(new LinkTraversalRetriever(params));\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\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) : 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: IIndexedMemoryEntry[] = [];\n for (const node of reached) {\n const match: IIndexedMemoryEntry | undefined = this._index.get(node);\n if (match !== undefined) {\n entries.push(match);\n }\n }\n const ordered: ReadonlyArray<IIndexedMemoryEntry> = entries\n .filter((entry) => indexedRecordMatchesQuery(entry, query))\n .sort(orderingCompare(query.orderBy));\n return materializePage(ordered, query, this._resolver);\n }\n\n /** Outbound neighbors: the scope-qualified targets of every edge on the record at `node`. */\n private _outbound(node: IEdgeTarget): IEdgeTarget[] {\n const targets: IEdgeTarget[] = [];\n const match: IIndexedMemoryEntry | undefined = this._index.get(node);\n if (match !== undefined) {\n for (const edge of match.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"]}
@@ -1,7 +1,6 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
2
  import { IMemoryRecord } from '../types';
3
- import { IMemoryIndex } from '../index';
4
- import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './retriever';
3
+ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities, IRetrieverCreateParams } from './retriever';
5
4
  /**
6
5
  * Returns records matching the query's scope / kind / tag / provenance-source / predicate filters,
7
6
  * ordered most-recently-updated first. The universal v1 retriever — an empty
@@ -10,11 +9,12 @@ import { IMemoryQuery, IMemoryRetriever, IMemoryRetrieverCapabilities } from './
10
9
  */
11
10
  export declare class RecencyRetriever implements IMemoryRetriever {
12
11
  private readonly _index;
12
+ private readonly _resolver;
13
13
  private constructor();
14
14
  /** {@inheritDoc IMemoryRetriever.capabilities} */
15
15
  get capabilities(): IMemoryRetrieverCapabilities;
16
16
  /** Family-convention factory. */
17
- static create(index: IMemoryIndex): Result<RecencyRetriever>;
17
+ static create(params: IRetrieverCreateParams): Result<RecencyRetriever>;
18
18
  /** {@inheritDoc IMemoryRetriever.retrieve} */
19
19
  retrieve(query: IMemoryQuery): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
20
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"recencyRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,gBAAgB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO;IAIP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,iCAAiC;WACnB,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAInE,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CAU7F"}
1
+ {"version":3,"file":"recencyRetriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAyB,MAAM,UAAU,CAAC;AAEhE,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,4BAA4B,EAC5B,sBAAsB,EAIvB,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,gBAAgB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,OAAO;IAKP,kDAAkD;IAClD,IAAW,YAAY,IAAI,4BAA4B,CAEtD;IAED,iCAAiC;WACnB,MAAM,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAI9E,8CAA8C;IACvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CAO7F"}
@@ -14,22 +14,22 @@ const retriever_1 = require("./retriever");
14
14
  * @public
15
15
  */
16
16
  class RecencyRetriever {
17
- constructor(index) {
18
- this._index = index;
17
+ constructor(params) {
18
+ this._index = params.index;
19
+ this._resolver = params.resolver;
19
20
  }
20
21
  /** {@inheritDoc IMemoryRetriever.capabilities} */
21
22
  get capabilities() {
22
23
  return retriever_1.NON_SEMANTIC_CAPABILITIES;
23
24
  }
24
25
  /** Family-convention factory. */
25
- static create(index) {
26
- return (0, ts_utils_1.succeed)(new RecencyRetriever(index));
26
+ static create(params) {
27
+ return (0, ts_utils_1.succeed)(new RecencyRetriever(params));
27
28
  }
28
29
  /** {@inheritDoc IMemoryRetriever.retrieve} */
29
30
  retrieve(query) {
30
31
  return Promise.resolve((0, retriever_1.guardRetrieverCapabilities)(query, this.capabilities).onSuccess(() => {
31
- const ordered = (0, retriever_1.selectByQuery)(this._index.entries(), query).sort((0, retriever_1.orderingCompare)(query.orderBy));
32
- return (0, ts_utils_1.succeed)((0, retriever_1.limitRecords)(ordered, query.limit, query.offset));
32
+ return (0, retriever_1.resolveQuery)(this._index.entries(), query, this._resolver);
33
33
  }));
34
34
  }
35
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAgD;AAGhD,2CASqB;AAErB;;;;;GAKG;AACH,MAAa,gBAAgB;IAG3B,YAAoB,KAAmB;QACrC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,KAAmB;QACtC,OAAO,IAAA,kBAAO,EAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,MAAM,OAAO,GAA6B,IAAA,yBAAa,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,IAAI,CACxF,IAAA,2BAAe,EAAC,KAAK,CAAC,OAAO,CAAC,CAC/B,CAAC;YACF,OAAO,IAAA,kBAAO,EAAC,IAAA,wBAAY,EAAC,OAAO,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA5BD,4CA4BC","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 / provenance-source / 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"]}
1
+ {"version":3,"file":"recencyRetriever.js","sourceRoot":"","sources":["../../../src/packlets/retrieve/recencyRetriever.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,4CAAgD;AAGhD,2CAQqB;AAErB;;;;;GAKG;AACH,MAAa,gBAAgB;IAI3B,YAAoB,MAA8B;QAChD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC;IACnC,CAAC;IAED,kDAAkD;IAClD,IAAW,YAAY;QACrB,OAAO,qCAAyB,CAAC;IACnC,CAAC;IAED,iCAAiC;IAC1B,MAAM,CAAC,MAAM,CAAC,MAA8B;QACjD,OAAO,IAAA,kBAAO,EAAC,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,8CAA8C;IACvC,QAAQ,CAAC,KAAmB;QACjC,OAAO,OAAO,CAAC,OAAO,CACpB,IAAA,sCAA0B,EAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE;YAClE,OAAO,IAAA,wBAAY,EAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACpE,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA3BD,4CA2BC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, succeed } from '@fgv/ts-utils';\nimport { IMemoryRecord, IMemoryRecordResolver } from '../types';\nimport { IMemoryIndex } from '../index';\nimport {\n IMemoryQuery,\n IMemoryRetriever,\n IMemoryRetrieverCapabilities,\n IRetrieverCreateParams,\n NON_SEMANTIC_CAPABILITIES,\n guardRetrieverCapabilities,\n resolveQuery\n} from './retriever';\n\n/**\n * Returns records matching the query's scope / kind / tag / provenance-source / 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 private readonly _resolver: IMemoryRecordResolver;\n\n private constructor(params: IRetrieverCreateParams) {\n this._index = params.index;\n this._resolver = params.resolver;\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(params: IRetrieverCreateParams): Result<RecencyRetriever> {\n return succeed(new RecencyRetriever(params));\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 return resolveQuery(this._index.entries(), query, this._resolver);\n })\n );\n }\n}\n"]}
@@ -1,6 +1,23 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
- import { IEdgeTarget, IMemoryRecord, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';
3
- import { IIndexedMemoryRecord } from '../index';
2
+ import { IEdgeTarget, IEnvelopeCarrier, IMemoryRecord, IMemoryRecordResolver, Kind, MemoryScopeKey, ProvenanceSource, Tag } from '../types';
3
+ import { IIndexedMemoryEntry, IMemoryIndex } from '../index';
4
+ /**
5
+ * What every index-backed retriever needs: the index it selects over, and the
6
+ * resolver it materializes survivors through.
7
+ *
8
+ * @remarks
9
+ * The resolver is the whole of the partial-read migration for a caller that
10
+ * constructs retrievers directly — the index projection itself is invisible to
11
+ * anyone who only consumes `retrieve()`. `FileTreeMemoryStore` implements
12
+ * {@link IMemoryRecordResolver}, so wiring is `{ index, resolver: store }`.
13
+ * @public
14
+ */
15
+ export interface IRetrieverCreateParams {
16
+ /** The derived index to select over. */
17
+ readonly index: IMemoryIndex;
18
+ /** Materializes the selected entries' bodies. */
19
+ readonly resolver: IMemoryRecordResolver;
20
+ }
4
21
  /**
5
22
  * The capabilities a retriever exposes. A consumer probes these before
6
23
  * dispatching a query so it never silently gets an empty result for a
@@ -161,7 +178,7 @@ export declare function guardRetrieverCapabilities(query: IMemoryQuery, capabili
161
178
  * equal-`updated` records sort deterministically. Mirrors the B1 index ordering.
162
179
  * @public
163
180
  */
164
- export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number;
181
+ export declare function recencyCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
165
182
  /**
166
183
  * Rank comparator: store-computed {@link IMemoryEnvelope.rank} descending, with
167
184
  * {@link recencyCompare} as the tiebreak. Records with an absent `rank` sort LAST
@@ -169,14 +186,14 @@ export declare function recencyCompare(a: IMemoryRecord<unknown>, b: IMemoryReco
169
186
  * index's rank-view ordering.
170
187
  * @public
171
188
  */
172
- export declare function rankCompare(a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>): number;
189
+ export declare function rankCompare(a: IEnvelopeCarrier, b: IEnvelopeCarrier): number;
173
190
  /**
174
191
  * Select the record comparator for a query's {@link IMemoryQuery.orderBy | orderBy}
175
192
  * axis: {@link rankCompare} for `'rank'`, {@link recencyCompare} otherwise (the
176
193
  * default, byte-identical to the pre-`orderBy` behavior).
177
194
  * @public
178
195
  */
179
- export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IMemoryRecord<unknown>, b: IMemoryRecord<unknown>) => number;
196
+ export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a: IEnvelopeCarrier, b: IEnvelopeCarrier) => number;
180
197
  /**
181
198
  * Whether an indexed entry satisfies a query's scope / kind / tag /
182
199
  * provenance-source / predicate pre-filter (the axes shared by every v1
@@ -184,14 +201,78 @@ export declare function orderingCompare(orderBy?: IMemoryQuery['orderBy']): (a:
184
201
  * are each retriever's own concern.
185
202
  * @public
186
203
  */
187
- export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryRecord, query: IMemoryQuery): boolean;
204
+ export declare function indexedRecordMatchesQuery(entry: IIndexedMemoryEntry, query: IMemoryQuery): boolean;
188
205
  /**
189
206
  * Apply the shared scope / kind / tag / provenance-source / predicate pre-filter
190
207
  * to a set of indexed entries, returning the surviving records (unordered,
191
208
  * unlimited).
192
209
  * @public
193
210
  */
194
- export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryRecord>, query: IMemoryQuery): IMemoryRecord<unknown>[];
211
+ export declare function selectByQuery(entries: ReadonlyArray<IIndexedMemoryEntry>, query: IMemoryQuery): IIndexedMemoryEntry[];
212
+ /**
213
+ * The shared select → order → page → materialize pipeline every non-semantic
214
+ * retriever runs, and the one place the body-vs-envelope ordering decision lives.
215
+ *
216
+ * @public
217
+ * @remarks
218
+ * **Without `query.filter`, ordering and paging happen over envelopes and only
219
+ * the page is materialized** — so `limit` genuinely bounds the number of records
220
+ * read, not merely the number returned. That is what makes `limit` a legitimate
221
+ * narrowing axis rather than a loophole, and it is conditional on the ordering
222
+ * key being an envelope field: both shipped comparators (`recencyCompare`,
223
+ * `rankCompare`) read `updated` / `seq` / `rank` and qualify. **An ordering that
224
+ * keyed on a body field could not use this path** and would have to sort after
225
+ * materialization, at which point `limit` bounds the result and not the read.
226
+ *
227
+ * **With `query.filter`, every envelope-survivor must be materialized first**,
228
+ * because the predicate takes a whole record. Semantics are preserved exactly;
229
+ * the cost is not. Pair `filter` with an envelope axis when the read cost
230
+ * matters.
231
+ */
232
+ export declare function resolveQuery(entries: ReadonlyArray<IIndexedMemoryEntry>, query: IMemoryQuery, resolver: IMemoryRecordResolver): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
233
+ /**
234
+ * Materialize a selected, ordered page — **applying `query.filter` and paging in
235
+ * the right order**, which is the whole reason this is shared rather than
236
+ * open-coded per retriever.
237
+ *
238
+ * @remarks
239
+ * Two paths, and the choice is forced by where the predicate can run:
240
+ *
241
+ * - **No `filter`** — order and page over *envelopes*, then read only the page.
242
+ * `limit` bounds the READ, not just the result.
243
+ * - **With `filter`** — the predicate takes a whole record, so every
244
+ * envelope-survivor must be read first, then filtered, then paged. Paging
245
+ * before filtering would return fewer than `limit` rows for no reason a caller
246
+ * could see.
247
+ *
248
+ * **Every retriever must route through this.** `indexedRecordMatchesQuery`
249
+ * structurally *cannot* apply `filter` — it is handed an envelope — so a
250
+ * retriever that pre-filters with it and then materializes on its own silently
251
+ * ignores the predicate. That regression shipped once, in the stream that moved
252
+ * `filter` out of the pre-filter; this function exists so it cannot recur.
253
+ *
254
+ * @param selected - Entries surviving the envelope pre-filter.
255
+ * @param query - The query whose `filter` / `orderBy` / `limit` / `offset` apply.
256
+ * @param resolver - Body resolver.
257
+ * @param order - Applied to whichever collection is paged; identity is legal for
258
+ * a retriever whose ordering is intrinsic (semantic score, traversal order).
259
+ * @public
260
+ */
261
+ export declare function materializePage<T extends IIndexedMemoryEntry>(selected: ReadonlyArray<T>, query: IMemoryQuery, resolver: IMemoryRecordResolver, order?: (candidates: ReadonlyArray<T>) => ReadonlyArray<T>): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
262
+ /**
263
+ * Materialize entries through the resolver, dropping any that have vanished
264
+ * since selection — a concurrent delete between selecting an envelope and
265
+ * reading its body is a legitimate race and yields a shorter list, not an error.
266
+ * A read that FAILS is a real fault and propagates.
267
+ * @public
268
+ */
269
+ export declare function materializeEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, resolver: IMemoryRecordResolver): Result<ReadonlyArray<IMemoryRecord<unknown>>>;
270
+ /**
271
+ * The {@link limitRecords} window, applied to entries — used on the
272
+ * no-body-filter path so paging happens before anything is read.
273
+ * @public
274
+ */
275
+ export declare function limitEntries(entries: ReadonlyArray<IIndexedMemoryEntry>, limit?: number, offset?: number): ReadonlyArray<IIndexedMemoryEntry>;
195
276
  /**
196
277
  * Apply the `{ offset, limit }` page window to an ordered record set. Applied
197
278
  * last, after ordering, so it always takes a stable window of the ordered
@@ -1 +1 @@
1
- {"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"retriever.d.ts","sourceRoot":"","sources":["../../../src/packlets/retrieve/retriever.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,10 @@ exports.rankCompare = rankCompare;
12
12
  exports.orderingCompare = orderingCompare;
13
13
  exports.indexedRecordMatchesQuery = indexedRecordMatchesQuery;
14
14
  exports.selectByQuery = selectByQuery;
15
+ exports.resolveQuery = resolveQuery;
16
+ exports.materializePage = materializePage;
17
+ exports.materializeEntries = materializeEntries;
18
+ exports.limitEntries = limitEntries;
15
19
  exports.limitRecords = limitRecords;
16
20
  const ts_utils_1 = require("@fgv/ts-utils");
17
21
  /**
@@ -114,22 +118,21 @@ function indexedRecordMatchesQuery(entry, query) {
114
118
  if (query.scope !== undefined && entry.scope !== query.scope) {
115
119
  return false;
116
120
  }
117
- if (query.kind !== undefined && entry.record.envelope.kind !== query.kind) {
121
+ if (query.kind !== undefined && entry.envelope.kind !== query.kind) {
118
122
  return false;
119
123
  }
120
- if (query.kinds !== undefined && !query.kinds.includes(entry.record.envelope.kind)) {
124
+ if (query.kinds !== undefined && !query.kinds.includes(entry.envelope.kind)) {
121
125
  return false;
122
126
  }
123
- if (query.tag !== undefined && !entry.record.envelope.tags.includes(query.tag)) {
127
+ if (query.tag !== undefined && !entry.envelope.tags.includes(query.tag)) {
124
128
  return false;
125
129
  }
126
- if (query.provenanceSource !== undefined &&
127
- entry.record.envelope.provenance.source !== query.provenanceSource) {
128
- return false;
129
- }
130
- if (query.filter !== undefined && !query.filter(entry.record)) {
130
+ if (query.provenanceSource !== undefined && entry.envelope.provenance.source !== query.provenanceSource) {
131
131
  return false;
132
132
  }
133
+ // `query.filter` is deliberately NOT applied here — it is a predicate over the
134
+ // whole record, and the index holds envelopes. It runs after materialization;
135
+ // see `resolveQuery`.
133
136
  return true;
134
137
  }
135
138
  /**
@@ -139,7 +142,89 @@ function indexedRecordMatchesQuery(entry, query) {
139
142
  * @public
140
143
  */
141
144
  function selectByQuery(entries, query) {
142
- return entries.filter((entry) => indexedRecordMatchesQuery(entry, query)).map((entry) => entry.record);
145
+ return entries.filter((entry) => indexedRecordMatchesQuery(entry, query));
146
+ }
147
+ /**
148
+ * The shared select → order → page → materialize pipeline every non-semantic
149
+ * retriever runs, and the one place the body-vs-envelope ordering decision lives.
150
+ *
151
+ * @public
152
+ * @remarks
153
+ * **Without `query.filter`, ordering and paging happen over envelopes and only
154
+ * the page is materialized** — so `limit` genuinely bounds the number of records
155
+ * read, not merely the number returned. That is what makes `limit` a legitimate
156
+ * narrowing axis rather than a loophole, and it is conditional on the ordering
157
+ * key being an envelope field: both shipped comparators (`recencyCompare`,
158
+ * `rankCompare`) read `updated` / `seq` / `rank` and qualify. **An ordering that
159
+ * keyed on a body field could not use this path** and would have to sort after
160
+ * materialization, at which point `limit` bounds the result and not the read.
161
+ *
162
+ * **With `query.filter`, every envelope-survivor must be materialized first**,
163
+ * because the predicate takes a whole record. Semantics are preserved exactly;
164
+ * the cost is not. Pair `filter` with an envelope axis when the read cost
165
+ * matters.
166
+ */
167
+ function resolveQuery(entries, query, resolver) {
168
+ return materializePage(selectByQuery(entries, query), query, resolver, (candidates) => [...candidates].sort(orderingCompare(query.orderBy)));
169
+ }
170
+ /**
171
+ * Materialize a selected, ordered page — **applying `query.filter` and paging in
172
+ * the right order**, which is the whole reason this is shared rather than
173
+ * open-coded per retriever.
174
+ *
175
+ * @remarks
176
+ * Two paths, and the choice is forced by where the predicate can run:
177
+ *
178
+ * - **No `filter`** — order and page over *envelopes*, then read only the page.
179
+ * `limit` bounds the READ, not just the result.
180
+ * - **With `filter`** — the predicate takes a whole record, so every
181
+ * envelope-survivor must be read first, then filtered, then paged. Paging
182
+ * before filtering would return fewer than `limit` rows for no reason a caller
183
+ * could see.
184
+ *
185
+ * **Every retriever must route through this.** `indexedRecordMatchesQuery`
186
+ * structurally *cannot* apply `filter` — it is handed an envelope — so a
187
+ * retriever that pre-filters with it and then materializes on its own silently
188
+ * ignores the predicate. That regression shipped once, in the stream that moved
189
+ * `filter` out of the pre-filter; this function exists so it cannot recur.
190
+ *
191
+ * @param selected - Entries surviving the envelope pre-filter.
192
+ * @param query - The query whose `filter` / `orderBy` / `limit` / `offset` apply.
193
+ * @param resolver - Body resolver.
194
+ * @param order - Applied to whichever collection is paged; identity is legal for
195
+ * a retriever whose ordering is intrinsic (semantic score, traversal order).
196
+ * @public
197
+ */
198
+ function materializePage(selected, query, resolver, order) {
199
+ const ordered = order === undefined ? selected : order(selected);
200
+ if (query.filter === undefined) {
201
+ return materializeEntries(limitEntries(ordered, query.limit, query.offset), resolver);
202
+ }
203
+ const predicate = query.filter;
204
+ return materializeEntries(ordered, resolver).onSuccess((records) => (0, ts_utils_1.succeed)(limitRecords(records.filter(predicate), query.limit, query.offset)));
205
+ }
206
+ /**
207
+ * Materialize entries through the resolver, dropping any that have vanished
208
+ * since selection — a concurrent delete between selecting an envelope and
209
+ * reading its body is a legitimate race and yields a shorter list, not an error.
210
+ * A read that FAILS is a real fault and propagates.
211
+ * @public
212
+ */
213
+ function materializeEntries(entries, resolver) {
214
+ return (0, ts_utils_1.mapResults)(entries.map((e) => resolver.resolveRecord(e.scope, e.envelope.id))).onSuccess((records) => (0, ts_utils_1.succeed)(records.filter((r) => r !== undefined)));
215
+ }
216
+ /**
217
+ * The {@link limitRecords} window, applied to entries — used on the
218
+ * no-body-filter path so paging happens before anything is read.
219
+ * @public
220
+ */
221
+ function limitEntries(entries, limit, offset) {
222
+ const skip = offset !== undefined && offset > 0 ? offset : 0;
223
+ const windowed = skip > 0 ? entries.slice(skip) : entries;
224
+ if (limit === undefined) {
225
+ return windowed;
226
+ }
227
+ return limit > 0 ? windowed.slice(0, limit) : [];
143
228
  }
144
229
  /**
145
230
  * Apply the `{ offset, limit }` page window to an ordered record set. Applied