@graphorin/memory 0.5.0

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 (287) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/dist/conflict/index.d.ts +3 -0
  5. package/dist/conflict/index.js +7 -0
  6. package/dist/conflict/locale-packs/en.js +262 -0
  7. package/dist/conflict/locale-packs/en.js.map +1 -0
  8. package/dist/conflict/locale-packs/index.js +4 -0
  9. package/dist/conflict/locale-packs/types.d.ts +103 -0
  10. package/dist/conflict/locale-packs/types.d.ts.map +1 -0
  11. package/dist/conflict/locale-packs/types.js +55 -0
  12. package/dist/conflict/locale-packs/types.js.map +1 -0
  13. package/dist/conflict/pipeline.d.ts +35 -0
  14. package/dist/conflict/pipeline.d.ts.map +1 -0
  15. package/dist/conflict/pipeline.js +257 -0
  16. package/dist/conflict/pipeline.js.map +1 -0
  17. package/dist/conflict/stages/helpers.js +66 -0
  18. package/dist/conflict/stages/helpers.js.map +1 -0
  19. package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
  20. package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
  21. package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
  22. package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
  23. package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
  24. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
  25. package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
  26. package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
  27. package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
  28. package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
  29. package/dist/conflict/types.d.ts +222 -0
  30. package/dist/conflict/types.d.ts.map +1 -0
  31. package/dist/conflict/types.js +50 -0
  32. package/dist/conflict/types.js.map +1 -0
  33. package/dist/consolidator/budget.d.ts +1 -0
  34. package/dist/consolidator/budget.js +214 -0
  35. package/dist/consolidator/budget.js.map +1 -0
  36. package/dist/consolidator/decay.d.ts +35 -0
  37. package/dist/consolidator/decay.d.ts.map +1 -0
  38. package/dist/consolidator/decay.js +91 -0
  39. package/dist/consolidator/decay.js.map +1 -0
  40. package/dist/consolidator/dlq.js +58 -0
  41. package/dist/consolidator/dlq.js.map +1 -0
  42. package/dist/consolidator/errors.d.ts +54 -0
  43. package/dist/consolidator/errors.d.ts.map +1 -0
  44. package/dist/consolidator/errors.js +67 -0
  45. package/dist/consolidator/errors.js.map +1 -0
  46. package/dist/consolidator/idempotency.d.ts +1 -0
  47. package/dist/consolidator/idempotency.js +22 -0
  48. package/dist/consolidator/idempotency.js.map +1 -0
  49. package/dist/consolidator/index.d.ts +34 -0
  50. package/dist/consolidator/index.d.ts.map +1 -0
  51. package/dist/consolidator/index.js +163 -0
  52. package/dist/consolidator/index.js.map +1 -0
  53. package/dist/consolidator/lock.d.ts +2 -0
  54. package/dist/consolidator/lock.js +89 -0
  55. package/dist/consolidator/lock.js.map +1 -0
  56. package/dist/consolidator/noise-filter.d.ts +2 -0
  57. package/dist/consolidator/noise-filter.js +173 -0
  58. package/dist/consolidator/noise-filter.js.map +1 -0
  59. package/dist/consolidator/phases/deep.d.ts +4 -0
  60. package/dist/consolidator/phases/deep.js +198 -0
  61. package/dist/consolidator/phases/deep.js.map +1 -0
  62. package/dist/consolidator/phases/induce.d.ts +154 -0
  63. package/dist/consolidator/phases/induce.d.ts.map +1 -0
  64. package/dist/consolidator/phases/induce.js +253 -0
  65. package/dist/consolidator/phases/induce.js.map +1 -0
  66. package/dist/consolidator/phases/light.d.ts +4 -0
  67. package/dist/consolidator/phases/light.js +99 -0
  68. package/dist/consolidator/phases/light.js.map +1 -0
  69. package/dist/consolidator/phases/reflect.js +287 -0
  70. package/dist/consolidator/phases/reflect.js.map +1 -0
  71. package/dist/consolidator/phases/standard.d.ts +8 -0
  72. package/dist/consolidator/phases/standard.js +499 -0
  73. package/dist/consolidator/phases/standard.js.map +1 -0
  74. package/dist/consolidator/reconcile.js +192 -0
  75. package/dist/consolidator/reconcile.js.map +1 -0
  76. package/dist/consolidator/runtime.d.ts +62 -0
  77. package/dist/consolidator/runtime.d.ts.map +1 -0
  78. package/dist/consolidator/runtime.js +580 -0
  79. package/dist/consolidator/runtime.js.map +1 -0
  80. package/dist/consolidator/scheduler.d.ts +111 -0
  81. package/dist/consolidator/scheduler.d.ts.map +1 -0
  82. package/dist/consolidator/scheduler.js +71 -0
  83. package/dist/consolidator/scheduler.js.map +1 -0
  84. package/dist/consolidator/triggers.d.ts +40 -0
  85. package/dist/consolidator/triggers.d.ts.map +1 -0
  86. package/dist/consolidator/triggers.js +94 -0
  87. package/dist/consolidator/triggers.js.map +1 -0
  88. package/dist/consolidator/types.d.ts +397 -0
  89. package/dist/consolidator/types.d.ts.map +1 -0
  90. package/dist/consolidator/types.js +117 -0
  91. package/dist/consolidator/types.js.map +1 -0
  92. package/dist/context-engine/annotations.d.ts +118 -0
  93. package/dist/context-engine/annotations.d.ts.map +1 -0
  94. package/dist/context-engine/annotations.js +102 -0
  95. package/dist/context-engine/annotations.js.map +1 -0
  96. package/dist/context-engine/auto-recall.d.ts +52 -0
  97. package/dist/context-engine/auto-recall.d.ts.map +1 -0
  98. package/dist/context-engine/auto-recall.js +41 -0
  99. package/dist/context-engine/auto-recall.js.map +1 -0
  100. package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
  101. package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
  102. package/dist/context-engine/compaction/clear-tool-results.js +97 -0
  103. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
  104. package/dist/context-engine/compaction/compactor.d.ts +52 -0
  105. package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
  106. package/dist/context-engine/compaction/compactor.js +196 -0
  107. package/dist/context-engine/compaction/compactor.js.map +1 -0
  108. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
  109. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
  110. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
  111. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
  112. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
  113. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
  114. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
  115. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
  116. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
  117. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
  118. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
  119. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
  120. package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
  121. package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
  122. package/dist/context-engine/compaction/index.d.ts +9 -0
  123. package/dist/context-engine/compaction/index.js +9 -0
  124. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
  125. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
  126. package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
  127. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
  128. package/dist/context-engine/compaction/thresholds.d.ts +68 -0
  129. package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
  130. package/dist/context-engine/compaction/thresholds.js +63 -0
  131. package/dist/context-engine/compaction/thresholds.js.map +1 -0
  132. package/dist/context-engine/compaction/types.d.ts +202 -0
  133. package/dist/context-engine/compaction/types.d.ts.map +1 -0
  134. package/dist/context-engine/engine.d.ts +258 -0
  135. package/dist/context-engine/engine.d.ts.map +1 -0
  136. package/dist/context-engine/engine.js +424 -0
  137. package/dist/context-engine/engine.js.map +1 -0
  138. package/dist/context-engine/index.d.ts +68 -0
  139. package/dist/context-engine/index.d.ts.map +1 -0
  140. package/dist/context-engine/index.js +24 -0
  141. package/dist/context-engine/locale-packs/en.d.ts +14 -0
  142. package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
  143. package/dist/context-engine/locale-packs/en.js +87 -0
  144. package/dist/context-engine/locale-packs/en.js.map +1 -0
  145. package/dist/context-engine/locale-packs/index.js +4 -0
  146. package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
  147. package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
  148. package/dist/context-engine/locale-packs/resolver.js +119 -0
  149. package/dist/context-engine/locale-packs/resolver.js.map +1 -0
  150. package/dist/context-engine/locale-packs/types.d.ts +119 -0
  151. package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
  152. package/dist/context-engine/locale-packs/types.js +24 -0
  153. package/dist/context-engine/locale-packs/types.js.map +1 -0
  154. package/dist/context-engine/metadata.d.ts +40 -0
  155. package/dist/context-engine/metadata.d.ts.map +1 -0
  156. package/dist/context-engine/metadata.js +66 -0
  157. package/dist/context-engine/metadata.js.map +1 -0
  158. package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
  159. package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
  160. package/dist/context-engine/preambles/inbound-en.js +23 -0
  161. package/dist/context-engine/preambles/inbound-en.js.map +1 -0
  162. package/dist/context-engine/privacy-filter.d.ts +108 -0
  163. package/dist/context-engine/privacy-filter.d.ts.map +1 -0
  164. package/dist/context-engine/privacy-filter.js +113 -0
  165. package/dist/context-engine/privacy-filter.js.map +1 -0
  166. package/dist/context-engine/templates/base-en.d.ts +19 -0
  167. package/dist/context-engine/templates/base-en.d.ts.map +1 -0
  168. package/dist/context-engine/templates/base-en.js +22 -0
  169. package/dist/context-engine/templates/base-en.js.map +1 -0
  170. package/dist/context-engine/templates/composer.d.ts +55 -0
  171. package/dist/context-engine/templates/composer.d.ts.map +1 -0
  172. package/dist/context-engine/templates/composer.js +51 -0
  173. package/dist/context-engine/templates/composer.js.map +1 -0
  174. package/dist/context-engine/token-budget.d.ts +99 -0
  175. package/dist/context-engine/token-budget.d.ts.map +1 -0
  176. package/dist/context-engine/token-budget.js +211 -0
  177. package/dist/context-engine/token-budget.js.map +1 -0
  178. package/dist/context-engine/token-counter.d.ts +54 -0
  179. package/dist/context-engine/token-counter.d.ts.map +1 -0
  180. package/dist/context-engine/token-counter.js +81 -0
  181. package/dist/context-engine/token-counter.js.map +1 -0
  182. package/dist/errors/index.d.ts +171 -0
  183. package/dist/errors/index.d.ts.map +1 -0
  184. package/dist/errors/index.js +206 -0
  185. package/dist/errors/index.js.map +1 -0
  186. package/dist/facade.d.ts +278 -0
  187. package/dist/facade.d.ts.map +1 -0
  188. package/dist/facade.js +265 -0
  189. package/dist/facade.js.map +1 -0
  190. package/dist/graph/entity-resolver.d.ts +135 -0
  191. package/dist/graph/entity-resolver.d.ts.map +1 -0
  192. package/dist/graph/entity-resolver.js +202 -0
  193. package/dist/graph/entity-resolver.js.map +1 -0
  194. package/dist/graph/index.d.ts +1 -0
  195. package/dist/index.d.ts +103 -0
  196. package/dist/index.d.ts.map +1 -0
  197. package/dist/index.js +95 -0
  198. package/dist/index.js.map +1 -0
  199. package/dist/internal/contextualize.d.ts +14 -0
  200. package/dist/internal/contextualize.d.ts.map +1 -0
  201. package/dist/internal/contextualize.js +110 -0
  202. package/dist/internal/contextualize.js.map +1 -0
  203. package/dist/internal/embedder-binding.js +50 -0
  204. package/dist/internal/embedder-binding.js.map +1 -0
  205. package/dist/internal/id.js +24 -0
  206. package/dist/internal/id.js.map +1 -0
  207. package/dist/internal/injection-heuristics.js +64 -0
  208. package/dist/internal/injection-heuristics.js.map +1 -0
  209. package/dist/internal/spans.js +35 -0
  210. package/dist/internal/spans.js.map +1 -0
  211. package/dist/internal/storage-adapter.d.ts +742 -0
  212. package/dist/internal/storage-adapter.d.ts.map +1 -0
  213. package/dist/migration/embedder-migration.d.ts +124 -0
  214. package/dist/migration/embedder-migration.d.ts.map +1 -0
  215. package/dist/migration/embedder-migration.js +109 -0
  216. package/dist/migration/embedder-migration.js.map +1 -0
  217. package/dist/migration/index.d.ts +2 -0
  218. package/dist/migration/index.js +3 -0
  219. package/dist/search/explain.d.ts +74 -0
  220. package/dist/search/explain.d.ts.map +1 -0
  221. package/dist/search/explain.js +50 -0
  222. package/dist/search/explain.js.map +1 -0
  223. package/dist/search/index.d.ts +6 -0
  224. package/dist/search/index.js +6 -0
  225. package/dist/search/iterative.d.ts +223 -0
  226. package/dist/search/iterative.d.ts.map +1 -0
  227. package/dist/search/iterative.js +330 -0
  228. package/dist/search/iterative.js.map +1 -0
  229. package/dist/search/query-transform.d.ts +112 -0
  230. package/dist/search/query-transform.d.ts.map +1 -0
  231. package/dist/search/query-transform.js +184 -0
  232. package/dist/search/query-transform.js.map +1 -0
  233. package/dist/search/rrf.d.ts +95 -0
  234. package/dist/search/rrf.d.ts.map +1 -0
  235. package/dist/search/rrf.js +166 -0
  236. package/dist/search/rrf.js.map +1 -0
  237. package/dist/search/types.d.ts +50 -0
  238. package/dist/search/types.d.ts.map +1 -0
  239. package/dist/tiers/episodic-memory.d.ts +142 -0
  240. package/dist/tiers/episodic-memory.d.ts.map +1 -0
  241. package/dist/tiers/episodic-memory.js +254 -0
  242. package/dist/tiers/episodic-memory.js.map +1 -0
  243. package/dist/tiers/index.d.ts +8 -0
  244. package/dist/tiers/index.js +9 -0
  245. package/dist/tiers/insight-memory.d.ts +71 -0
  246. package/dist/tiers/insight-memory.d.ts.map +1 -0
  247. package/dist/tiers/insight-memory.js +133 -0
  248. package/dist/tiers/insight-memory.js.map +1 -0
  249. package/dist/tiers/procedural-memory.d.ts +147 -0
  250. package/dist/tiers/procedural-memory.d.ts.map +1 -0
  251. package/dist/tiers/procedural-memory.js +267 -0
  252. package/dist/tiers/procedural-memory.js.map +1 -0
  253. package/dist/tiers/semantic-memory.d.ts +461 -0
  254. package/dist/tiers/semantic-memory.d.ts.map +1 -0
  255. package/dist/tiers/semantic-memory.js +631 -0
  256. package/dist/tiers/semantic-memory.js.map +1 -0
  257. package/dist/tiers/session-memory.d.ts +117 -0
  258. package/dist/tiers/session-memory.d.ts.map +1 -0
  259. package/dist/tiers/session-memory.js +163 -0
  260. package/dist/tiers/session-memory.js.map +1 -0
  261. package/dist/tiers/shared-memory.d.ts +29 -0
  262. package/dist/tiers/shared-memory.d.ts.map +1 -0
  263. package/dist/tiers/shared-memory.js +58 -0
  264. package/dist/tiers/shared-memory.js.map +1 -0
  265. package/dist/tiers/working-memory.d.ts +129 -0
  266. package/dist/tiers/working-memory.d.ts.map +1 -0
  267. package/dist/tiers/working-memory.js +229 -0
  268. package/dist/tiers/working-memory.js.map +1 -0
  269. package/dist/tools/block-tools.d.ts +98 -0
  270. package/dist/tools/block-tools.d.ts.map +1 -0
  271. package/dist/tools/block-tools.js +114 -0
  272. package/dist/tools/block-tools.js.map +1 -0
  273. package/dist/tools/fact-tools.d.ts +294 -0
  274. package/dist/tools/fact-tools.d.ts.map +1 -0
  275. package/dist/tools/fact-tools.js +293 -0
  276. package/dist/tools/fact-tools.js.map +1 -0
  277. package/dist/tools/index.d.ts +35 -0
  278. package/dist/tools/index.d.ts.map +1 -0
  279. package/dist/tools/index.js +36 -0
  280. package/dist/tools/index.js.map +1 -0
  281. package/dist/tools/recall-tools.d.ts +209 -0
  282. package/dist/tools/recall-tools.d.ts.map +1 -0
  283. package/dist/tools/recall-tools.js +170 -0
  284. package/dist/tools/recall-tools.js.map +1 -0
  285. package/dist/tools/types.d.ts +40 -0
  286. package/dist/tools/types.d.ts.map +1 -0
  287. package/package.json +119 -0
@@ -0,0 +1,112 @@
1
+ import { Provider, ProviderRequest } from "@graphorin/core";
2
+
3
+ //#region src/search/query-transform.d.ts
4
+
5
+ /** Default ceiling on reworded variants a provider-backed transformer requests. */
6
+ declare const DEFAULT_MAX_QUERY_VARIANTS = 5;
7
+ /**
8
+ * Pluggable query-transformation seam consumed by
9
+ * `SemanticMemory.search(..., { multiQuery, hyde })`. The built-in
10
+ * provider-backed implementation lives in
11
+ * {@link createProviderQueryTransformer}; advanced callers can supply a
12
+ * bespoke one (e.g. a deterministic synonym expander) to
13
+ * `new SemanticMemory({ queryTransformer })`.
14
+ *
15
+ * Implementations MUST degrade gracefully — return `[]` / `null` rather
16
+ * than throw — so a transform failure never breaks recall.
17
+ *
18
+ * @stable
19
+ */
20
+ interface QueryTransformer {
21
+ /**
22
+ * Rewrite a query into up to `count` **additional** reworded variants
23
+ * (the original query is retained by the caller). Returns `[]` when
24
+ * transformation is unavailable or the model returns nothing usable.
25
+ */
26
+ expand(query: string, count: number, options?: QueryTransformOptions): Promise<ReadonlyArray<string>>;
27
+ /**
28
+ * Generate a single short hypothetical-answer passage to embed (HyDE),
29
+ * or `null` when unavailable. The caller embeds the passage and fuses
30
+ * its vector neighbours into the result.
31
+ */
32
+ hypothetical(query: string, options?: QueryTransformOptions): Promise<string | null>;
33
+ }
34
+ /**
35
+ * Per-call options for a {@link QueryTransformer}.
36
+ *
37
+ * @stable
38
+ */
39
+ interface QueryTransformOptions {
40
+ /** Cancellation signal forwarded to the underlying provider call. */
41
+ readonly signal?: AbortSignal;
42
+ }
43
+ /**
44
+ * System prompt for multi-query variant generation. Asks for a bare
45
+ * JSON array of standalone rephrasings that preserve the original
46
+ * intent — {@link parseQueryVariants} also tolerates a chatty model.
47
+ *
48
+ * @internal
49
+ */
50
+ declare const QUERY_EXPANSION_SYSTEM_PROMPT: string;
51
+ /**
52
+ * System prompt for HyDE. Asks for a short, plausible hypothetical
53
+ * answer (not a question rephrase) whose embedding sits near the
54
+ * passage that would actually answer the query.
55
+ *
56
+ * @internal
57
+ */
58
+ declare const HYDE_SYSTEM_PROMPT: string;
59
+ /**
60
+ * Build the multi-query expansion request. Pure — no I/O. A higher
61
+ * temperature is used deliberately so the variants diverge (the
62
+ * downstream retrieval + RRF fusion stays deterministic).
63
+ *
64
+ * @stable
65
+ */
66
+ declare function buildExpansionRequest(query: string, count: number, options?: {
67
+ readonly maxTokens?: number;
68
+ readonly signal?: AbortSignal;
69
+ }): ProviderRequest;
70
+ /**
71
+ * Build the HyDE pseudo-document request. Pure — no I/O.
72
+ *
73
+ * @stable
74
+ */
75
+ declare function buildHydeRequest(query: string, options?: {
76
+ readonly maxTokens?: number;
77
+ readonly signal?: AbortSignal;
78
+ }): ProviderRequest;
79
+ /**
80
+ * Parse the variant-generation model output into a deduped, capped list
81
+ * of reworded queries. Tolerates a JSON array, a `{ "variants": [...] }`
82
+ * / `{ "queries": [...] }` wrapper, fenced blocks, and (as a last
83
+ * resort) a newline / numbered list — so a chatty model never breaks
84
+ * recall. Empty strings and case-insensitive duplicates are dropped;
85
+ * the result is capped at `max`.
86
+ *
87
+ * @stable
88
+ */
89
+ declare function parseQueryVariants(text: string | undefined, max: number): ReadonlyArray<string>;
90
+ /**
91
+ * Parse the HyDE model output into a single passage, or `null` when the
92
+ * model returned nothing usable. Strips a fenced block and trims.
93
+ *
94
+ * @stable
95
+ */
96
+ declare function parseHypothetical(text: string | undefined): string | null;
97
+ /**
98
+ * Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods
99
+ * are **resilient**: a provider error or unparseable response degrades
100
+ * to `[]` / `null` so transformation never throws into `search`. The
101
+ * `maxVariants` ceiling caps the variants requested regardless of the
102
+ * caller's `multiQuery` value (a latency guardrail).
103
+ *
104
+ * @stable
105
+ */
106
+ declare function createProviderQueryTransformer(provider: Provider, options?: {
107
+ readonly maxVariants?: number;
108
+ readonly maxTokens?: number;
109
+ }): QueryTransformer;
110
+ //#endregion
111
+ export { DEFAULT_MAX_QUERY_VARIANTS, HYDE_SYSTEM_PROMPT, QUERY_EXPANSION_SYSTEM_PROMPT, QueryTransformOptions, QueryTransformer, buildExpansionRequest, buildHydeRequest, createProviderQueryTransformer, parseHypothetical, parseQueryVariants };
112
+ //# sourceMappingURL=query-transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-transform.d.ts","names":[],"sources":["../../src/search/query-transform.ts"],"sourcesContent":[],"mappings":";;;;;cA2Ba,0BAAA;;;;;;;;;;;;;;UAkBI,gBAAA;;;;;;iDASH,wBACT,QAAQ;;;;;;wCAM2B,wBAAwB;;;;;;;UAQ/C,qBAAA;;oBAEG;;;;;;;;;cAUP;;;;;;;;cAcA;;;;;;;;iBAYG,qBAAA;;oBAG4C;IACzD;;;;;;iBAqBa,gBAAA;;oBAE4C;IACzD;;;;;;;;;;;iBAoBa,kBAAA,yCAA2D;;;;;;;iBAyB3D,iBAAA;;;;;;;;;;iBAeA,8BAAA,WACJ;;;IAET"}
@@ -0,0 +1,184 @@
1
+ //#region src/search/query-transform.ts
2
+ /** Default ceiling on reworded variants a provider-backed transformer requests. */
3
+ const DEFAULT_MAX_QUERY_VARIANTS = 5;
4
+ /** Default output-token ceiling for a single transform call. */
5
+ const DEFAULT_TRANSFORM_MAX_TOKENS = 256;
6
+ /**
7
+ * System prompt for multi-query variant generation. Asks for a bare
8
+ * JSON array of standalone rephrasings that preserve the original
9
+ * intent — {@link parseQueryVariants} also tolerates a chatty model.
10
+ *
11
+ * @internal
12
+ */
13
+ const QUERY_EXPANSION_SYSTEM_PROMPT = "You rewrite a search query into alternative phrasings to improve recall over a personal memory store. Preserve the original intent; vary vocabulary, specificity, and perspective so different wordings of the same stored memory are matched. Return ONLY a JSON array of strings — each a standalone query, no numbering, no commentary, no duplicates of the original.";
14
+ /**
15
+ * System prompt for HyDE. Asks for a short, plausible hypothetical
16
+ * answer (not a question rephrase) whose embedding sits near the
17
+ * passage that would actually answer the query.
18
+ *
19
+ * @internal
20
+ */
21
+ const HYDE_SYSTEM_PROMPT = "Write a short, plausible hypothetical answer to the question, phrased as a first-person statement of fact as it might be recorded in a memory store. One to three sentences. Output only the passage — no preamble, no caveats, no markdown.";
22
+ /**
23
+ * Build the multi-query expansion request. Pure — no I/O. A higher
24
+ * temperature is used deliberately so the variants diverge (the
25
+ * downstream retrieval + RRF fusion stays deterministic).
26
+ *
27
+ * @stable
28
+ */
29
+ function buildExpansionRequest(query, count, options = {}) {
30
+ return {
31
+ messages: [{
32
+ role: "user",
33
+ content: `Original query: ${query}\nReturn up to ${count} alternative phrasings as a JSON array of strings.`
34
+ }],
35
+ systemMessage: QUERY_EXPANSION_SYSTEM_PROMPT,
36
+ temperature: .7,
37
+ maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,
38
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
39
+ outputType: { kind: "structured" }
40
+ };
41
+ }
42
+ /**
43
+ * Build the HyDE pseudo-document request. Pure — no I/O.
44
+ *
45
+ * @stable
46
+ */
47
+ function buildHydeRequest(query, options = {}) {
48
+ return {
49
+ messages: [{
50
+ role: "user",
51
+ content: query
52
+ }],
53
+ systemMessage: HYDE_SYSTEM_PROMPT,
54
+ temperature: .3,
55
+ maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,
56
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
57
+ };
58
+ }
59
+ /**
60
+ * Parse the variant-generation model output into a deduped, capped list
61
+ * of reworded queries. Tolerates a JSON array, a `{ "variants": [...] }`
62
+ * / `{ "queries": [...] }` wrapper, fenced blocks, and (as a last
63
+ * resort) a newline / numbered list — so a chatty model never breaks
64
+ * recall. Empty strings and case-insensitive duplicates are dropped;
65
+ * the result is capped at `max`.
66
+ *
67
+ * @stable
68
+ */
69
+ function parseQueryVariants(text, max) {
70
+ if (max <= 0 || text === void 0) return [];
71
+ const candidate = stripFence(text).trim();
72
+ if (candidate.length === 0) return [];
73
+ const raw = extractVariantStrings(candidate);
74
+ const seen = /* @__PURE__ */ new Set();
75
+ const out = [];
76
+ for (const item of raw) {
77
+ const trimmed = item.trim();
78
+ if (trimmed.length === 0) continue;
79
+ const key = trimmed.toLowerCase();
80
+ if (seen.has(key)) continue;
81
+ seen.add(key);
82
+ out.push(trimmed);
83
+ if (out.length >= max) break;
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * Parse the HyDE model output into a single passage, or `null` when the
89
+ * model returned nothing usable. Strips a fenced block and trims.
90
+ *
91
+ * @stable
92
+ */
93
+ function parseHypothetical(text) {
94
+ if (text === void 0) return null;
95
+ const trimmed = stripFence(text).trim();
96
+ return trimmed.length === 0 ? null : trimmed;
97
+ }
98
+ /**
99
+ * Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods
100
+ * are **resilient**: a provider error or unparseable response degrades
101
+ * to `[]` / `null` so transformation never throws into `search`. The
102
+ * `maxVariants` ceiling caps the variants requested regardless of the
103
+ * caller's `multiQuery` value (a latency guardrail).
104
+ *
105
+ * @stable
106
+ */
107
+ function createProviderQueryTransformer(provider, options = {}) {
108
+ const maxVariants = Math.max(0, options.maxVariants ?? DEFAULT_MAX_QUERY_VARIANTS);
109
+ const maxTokens = options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS;
110
+ return {
111
+ async expand(query, count, opts = {}) {
112
+ const want = Math.min(Math.max(0, count), maxVariants);
113
+ if (want === 0 || query.trim().length === 0) return [];
114
+ try {
115
+ const request = buildExpansionRequest(query, want, {
116
+ maxTokens,
117
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
118
+ });
119
+ return parseQueryVariants((await provider.generate(request)).text, want);
120
+ } catch {
121
+ return [];
122
+ }
123
+ },
124
+ async hypothetical(query, opts = {}) {
125
+ if (query.trim().length === 0) return null;
126
+ try {
127
+ const request = buildHydeRequest(query, {
128
+ maxTokens,
129
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
130
+ });
131
+ return parseHypothetical((await provider.generate(request)).text);
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ };
137
+ }
138
+ /** Extract the raw (pre-dedup) variant strings from tolerant model output. */
139
+ function extractVariantStrings(candidate) {
140
+ const parsed = tryParseJson(candidate);
141
+ if (parsed !== void 0) {
142
+ const arr = Array.isArray(parsed) ? parsed : isRecord(parsed) ? pickArray(parsed) : null;
143
+ if (arr !== null) return arr.filter((v) => typeof v === "string");
144
+ }
145
+ return candidate.split("\n").map((line) => line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/u, "").replace(/^["']|["']$/gu, "").trim());
146
+ }
147
+ function pickArray(record) {
148
+ if (Array.isArray(record.variants)) return record.variants;
149
+ if (Array.isArray(record.queries)) return record.queries;
150
+ return null;
151
+ }
152
+ function tryParseJson(text) {
153
+ try {
154
+ return JSON.parse(text);
155
+ } catch {
156
+ const slice = sliceJson(text);
157
+ if (slice === null) return void 0;
158
+ try {
159
+ return JSON.parse(slice);
160
+ } catch {
161
+ return;
162
+ }
163
+ }
164
+ }
165
+ /** Slice the widest `[...]` array or `{...}` object out of chatty text. */
166
+ function sliceJson(text) {
167
+ const arrStart = text.indexOf("[");
168
+ const arrEnd = text.lastIndexOf("]");
169
+ if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);
170
+ const objStart = text.indexOf("{");
171
+ const objEnd = text.lastIndexOf("}");
172
+ if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);
173
+ return null;
174
+ }
175
+ function stripFence(text) {
176
+ return /^```[^\n]*\n([\s\S]*?)\n```/u.exec(text.trim())?.[1] ?? text;
177
+ }
178
+ function isRecord(value) {
179
+ return value !== null && typeof value === "object";
180
+ }
181
+
182
+ //#endregion
183
+ export { DEFAULT_MAX_QUERY_VARIANTS, HYDE_SYSTEM_PROMPT, QUERY_EXPANSION_SYSTEM_PROMPT, buildExpansionRequest, buildHydeRequest, createProviderQueryTransformer, parseHypothetical, parseQueryVariants };
184
+ //# sourceMappingURL=query-transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-transform.js","names":["out: string[]"],"sources":["../../src/search/query-transform.ts"],"sourcesContent":["/**\n * Query transformation for hybrid retrieval (P2-3) — **multi-query /\n * RAG-Fusion** and optional **HyDE**.\n *\n * Auto-recall is a regex trigger and the default search is single-shot:\n * the user's phrasing rarely matches how a memory was stored, so a\n * relevant fact is missed at the retrieval step. This module fans a\n * query into a few reworded variants (one cheap LLM call) and/or writes\n * a hypothetical answer passage to embed (HyDE, arXiv:2212.10496); the\n * caller retrieves each and fuses the lists through the existing RRF\n * reranker. Variant generation is the only thing that touches a\n * provider, so it is **opt-in**: with no transformer configured the\n * default search path stays fully offline and single-shot.\n *\n * The module is provider-agnostic — it imports only `@graphorin/core`\n * types and never performs I/O itself. The concrete `Provider` is\n * injected by the caller via {@link createProviderQueryTransformer},\n * which is resilient: any provider error or unparseable output degrades\n * to \"no variants\" / \"no hypothetical\" rather than throwing into the\n * search hot path.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\n\n/** Default ceiling on reworded variants a provider-backed transformer requests. */\nexport const DEFAULT_MAX_QUERY_VARIANTS = 5;\n\n/** Default output-token ceiling for a single transform call. */\nconst DEFAULT_TRANSFORM_MAX_TOKENS = 256;\n\n/**\n * Pluggable query-transformation seam consumed by\n * `SemanticMemory.search(..., { multiQuery, hyde })`. The built-in\n * provider-backed implementation lives in\n * {@link createProviderQueryTransformer}; advanced callers can supply a\n * bespoke one (e.g. a deterministic synonym expander) to\n * `new SemanticMemory({ queryTransformer })`.\n *\n * Implementations MUST degrade gracefully — return `[]` / `null` rather\n * than throw — so a transform failure never breaks recall.\n *\n * @stable\n */\nexport interface QueryTransformer {\n /**\n * Rewrite a query into up to `count` **additional** reworded variants\n * (the original query is retained by the caller). Returns `[]` when\n * transformation is unavailable or the model returns nothing usable.\n */\n expand(\n query: string,\n count: number,\n options?: QueryTransformOptions,\n ): Promise<ReadonlyArray<string>>;\n /**\n * Generate a single short hypothetical-answer passage to embed (HyDE),\n * or `null` when unavailable. The caller embeds the passage and fuses\n * its vector neighbours into the result.\n */\n hypothetical(query: string, options?: QueryTransformOptions): Promise<string | null>;\n}\n\n/**\n * Per-call options for a {@link QueryTransformer}.\n *\n * @stable\n */\nexport interface QueryTransformOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * System prompt for multi-query variant generation. Asks for a bare\n * JSON array of standalone rephrasings that preserve the original\n * intent — {@link parseQueryVariants} also tolerates a chatty model.\n *\n * @internal\n */\nexport const QUERY_EXPANSION_SYSTEM_PROMPT =\n 'You rewrite a search query into alternative phrasings to improve recall over a ' +\n 'personal memory store. Preserve the original intent; vary vocabulary, specificity, ' +\n 'and perspective so different wordings of the same stored memory are matched. ' +\n 'Return ONLY a JSON array of strings — each a standalone query, no numbering, no ' +\n 'commentary, no duplicates of the original.';\n\n/**\n * System prompt for HyDE. Asks for a short, plausible hypothetical\n * answer (not a question rephrase) whose embedding sits near the\n * passage that would actually answer the query.\n *\n * @internal\n */\nexport const HYDE_SYSTEM_PROMPT =\n 'Write a short, plausible hypothetical answer to the question, phrased as a ' +\n 'first-person statement of fact as it might be recorded in a memory store. One to ' +\n 'three sentences. Output only the passage — no preamble, no caveats, no markdown.';\n\n/**\n * Build the multi-query expansion request. Pure — no I/O. A higher\n * temperature is used deliberately so the variants diverge (the\n * downstream retrieval + RRF fusion stays deterministic).\n *\n * @stable\n */\nexport function buildExpansionRequest(\n query: string,\n count: number,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Original query: ${query}\\nReturn up to ${count} alternative phrasings as a JSON array of strings.`,\n },\n ],\n systemMessage: QUERY_EXPANSION_SYSTEM_PROMPT,\n temperature: 0.7,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Build the HyDE pseudo-document request. Pure — no I/O.\n *\n * @stable\n */\nexport function buildHydeRequest(\n query: string,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [{ role: 'user', content: query }],\n systemMessage: HYDE_SYSTEM_PROMPT,\n temperature: 0.3,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n };\n}\n\n/**\n * Parse the variant-generation model output into a deduped, capped list\n * of reworded queries. Tolerates a JSON array, a `{ \"variants\": [...] }`\n * / `{ \"queries\": [...] }` wrapper, fenced blocks, and (as a last\n * resort) a newline / numbered list — so a chatty model never breaks\n * recall. Empty strings and case-insensitive duplicates are dropped;\n * the result is capped at `max`.\n *\n * @stable\n */\nexport function parseQueryVariants(text: string | undefined, max: number): ReadonlyArray<string> {\n if (max <= 0 || text === undefined) return [];\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return [];\n const raw = extractVariantStrings(candidate);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of raw) {\n const trimmed = item.trim();\n if (trimmed.length === 0) continue;\n const key = trimmed.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(trimmed);\n if (out.length >= max) break;\n }\n return out;\n}\n\n/**\n * Parse the HyDE model output into a single passage, or `null` when the\n * model returned nothing usable. Strips a fenced block and trims.\n *\n * @stable\n */\nexport function parseHypothetical(text: string | undefined): string | null {\n if (text === undefined) return null;\n const trimmed = stripFence(text).trim();\n return trimmed.length === 0 ? null : trimmed;\n}\n\n/**\n * Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods\n * are **resilient**: a provider error or unparseable response degrades\n * to `[]` / `null` so transformation never throws into `search`. The\n * `maxVariants` ceiling caps the variants requested regardless of the\n * caller's `multiQuery` value (a latency guardrail).\n *\n * @stable\n */\nexport function createProviderQueryTransformer(\n provider: Provider,\n options: { readonly maxVariants?: number; readonly maxTokens?: number } = {},\n): QueryTransformer {\n const maxVariants = Math.max(0, options.maxVariants ?? DEFAULT_MAX_QUERY_VARIANTS);\n const maxTokens = options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS;\n return {\n async expand(query, count, opts = {}): Promise<ReadonlyArray<string>> {\n const want = Math.min(Math.max(0, count), maxVariants);\n if (want === 0 || query.trim().length === 0) return [];\n try {\n const request = buildExpansionRequest(query, want, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseQueryVariants(response.text, want);\n } catch {\n return [];\n }\n },\n async hypothetical(query, opts = {}): Promise<string | null> {\n if (query.trim().length === 0) return null;\n try {\n const request = buildHydeRequest(query, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseHypothetical(response.text);\n } catch {\n return null;\n }\n },\n };\n}\n\n/** Extract the raw (pre-dedup) variant strings from tolerant model output. */\nfunction extractVariantStrings(candidate: string): ReadonlyArray<string> {\n const parsed = tryParseJson(candidate);\n if (parsed !== undefined) {\n const arr = Array.isArray(parsed) ? parsed : isRecord(parsed) ? pickArray(parsed) : null;\n if (arr !== null) {\n return arr.filter((v): v is string => typeof v === 'string');\n }\n }\n // Fallback — split a newline / numbered list and strip list markers.\n return candidate.split('\\n').map((line) =>\n line\n .replace(/^\\s*(?:[-*•]|\\d+[.)])\\s*/u, '')\n .replace(/^[\"']|[\"']$/gu, '')\n .trim(),\n );\n}\n\nfunction pickArray(record: Record<string, unknown>): ReadonlyArray<unknown> | null {\n if (Array.isArray(record.variants)) return record.variants;\n if (Array.isArray(record.queries)) return record.queries;\n return null;\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n const slice = sliceJson(text);\n if (slice === null) return undefined;\n try {\n return JSON.parse(slice);\n } catch {\n return undefined;\n }\n }\n}\n\n/** Slice the widest `[...]` array or `{...}` object out of chatty text. */\nfunction sliceJson(text: string): string | null {\n const arrStart = text.indexOf('[');\n const arrEnd = text.lastIndexOf(']');\n if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);\n const objStart = text.indexOf('{');\n const objEnd = text.lastIndexOf('}');\n if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);\n return null;\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object';\n}\n"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import { ReRankOptions, ReRanker } from "./types.js";
2
+ import { MemoryHit, MemoryRecord } from "@graphorin/core";
3
+
4
+ //#region src/search/rrf.d.ts
5
+
6
+ /**
7
+ * Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes
8
+ * top-ranked items dominate; higher `k` smooths the contribution of
9
+ * lower-ranked items. `k = 60` is the value popularised by Cormack,
10
+ * Clarke, and Büttcher (2009) and is the framework default.
11
+ *
12
+ * @stable
13
+ */
14
+ declare const RRF_DEFAULT_K = 60;
15
+ /**
16
+ * Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked
17
+ * lists (vector hits, FTS5 hits, optional entity boost) into a single
18
+ * fused ranking by summing `1 / (k + rank)` per list each item appears
19
+ * in.
20
+ *
21
+ * Properties (verified by the property-based test suite):
22
+ *
23
+ * - **Deterministic.** Identical input lists yield identical output.
24
+ * - **Stable under permutation of input lists.** The fusion is
25
+ * independent of the order in which the input lists are passed in.
26
+ * - **Tie-broken by stable record id.** Records with equal RRF
27
+ * scores keep their first-seen order.
28
+ *
29
+ * @stable
30
+ */
31
+ declare class RRFReranker implements ReRanker {
32
+ readonly id: "rrf";
33
+ readonly k: number;
34
+ constructor(k?: number);
35
+ rerank<TRecord extends MemoryRecord>(query: string, lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>, options?: ReRankOptions): Promise<ReadonlyArray<MemoryHit<TRecord>>>;
36
+ }
37
+ /**
38
+ * Weighted-RRF reranker (X-2). Fuses parallel ranked lists through
39
+ * {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who
40
+ * has calibrated list reliability against labels (the P0-1 eval harness)
41
+ * can trust one retriever over another — e.g. up-weight dense vector
42
+ * hits over lexical FTS hits. `weights` is **positional**: aligned to the
43
+ * input `lists` order (the built-in hybrid search passes FTS first, then
44
+ * vector). At equal weights it is identical to {@link RRFReranker}; RRF
45
+ * stays the framework default.
46
+ *
47
+ * Like the RRF reranker it is deterministic and tie-broken by stable
48
+ * record id, and it preserves the upstream `signals` on each hit.
49
+ *
50
+ * @stable
51
+ */
52
+ declare class WeightedRRFReranker implements ReRanker {
53
+ readonly id: "weighted-rrf";
54
+ readonly k: number;
55
+ readonly weights: ReadonlyArray<number>;
56
+ constructor(args: {
57
+ weights: ReadonlyArray<number>;
58
+ k?: number;
59
+ });
60
+ rerank<TRecord extends MemoryRecord>(query: string, lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>, options?: ReRankOptions): Promise<ReadonlyArray<MemoryHit<TRecord>>>;
61
+ }
62
+ /**
63
+ * Pure functional core of the RRF reranker — the equal-weight case of
64
+ * {@link fuseWeighted}. Exported separately so the test suite (and the
65
+ * property-based fuzzer) can exercise the math without the `Promise<…>`
66
+ * wrapping of the public surface.
67
+ *
68
+ * @stable
69
+ */
70
+ declare function fuseRrf<TRecord extends MemoryRecord>(lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>, k: number, labels?: ReadonlyArray<string>): ReadonlyArray<MemoryHit<TRecord>>;
71
+ /**
72
+ * Weighted / convex generalization of {@link fuseRrf} (X-2). Each input
73
+ * list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused
74
+ * score instead of the flat `1 / (k + rank)`, so a caller who has
75
+ * calibrated list reliability against labels (the P0-1 eval harness) can
76
+ * trust one retriever over another. RRF stays the zero-tuning default: a
77
+ * `weights` of `undefined` (or all-`1`) is byte-for-byte identical to
78
+ * {@link fuseRrf}. A missing, non-finite, or negative entry falls back to
79
+ * the neutral weight `1`, so a partial / malformed `weights` never throws
80
+ * or poisons the ranking.
81
+ *
82
+ * Like {@link fuseRrf} the fusion is deterministic, tie-broken by
83
+ * first-seen order, and preserves any upstream `signals` (FTS `bm25`,
84
+ * vector similarity) the hits carried in — the `rrf` / `rrf.list_N`
85
+ * signals it records are the *weighted* contributions, so the fused
86
+ * `score` still equals their sum. Note that — unlike RRF — the result
87
+ * depends on input list *order*, because each weight is bound to a list
88
+ * position.
89
+ *
90
+ * @stable
91
+ */
92
+ declare function fuseWeighted<TRecord extends MemoryRecord>(lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>, weights: ReadonlyArray<number> | undefined, k: number, labels?: ReadonlyArray<string>): ReadonlyArray<MemoryHit<TRecord>>;
93
+ //#endregion
94
+ export { RRFReranker, RRF_DEFAULT_K, WeightedRRFReranker, fuseRrf, fuseWeighted };
95
+ //# sourceMappingURL=rrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rrf.d.ts","names":[],"sources":["../../src/search/rrf.ts"],"sourcesContent":[],"mappings":";;;;;;;AAWA;AAkBA;;;;;AAeW,cAjCE,aAAA,GAiCF,EAAA;;;;;;;;AA4BX;;;;;;;;;AA2BqC,cAtExB,WAAA,YAAuB,QAsEC,CAAA;EAAV,SAAA,EAAA,EAAA,KAAA;EAAd,SAAA,CAAA,EAAA,MAAA;EAAR,WAAA,CAAA,CAAA,CAAA,EAAA,MAAA;EA3BuC,MAAA,CAAA,gBA9Bb,YA8Ba,CAAA,CAAA,KAAA,EAAA,MAAA,EAAA,KAAA,EA5BjC,aA4BiC,CA5BnB,aA4BmB,CA5BL,SA4BK,CA5BK,OA4BL,CAAA,CAAA,CAAA,EAAA,OAAA,CAAA,EA3B/B,aA2B+B,CAAA,EA1BvC,OA0BuC,CA1B/B,aA0B+B,CA1BjB,SA0BiB,CA1BP,OA0BO,CAAA,CAAA,CAAA;;AA8C5C;;;;;;;;;;;AA6BA;;;;AACuB,cA5EV,mBAAA,YAA+B,QA4ErB,CAAA;EAAd,SAAA,EAAA,EAAA,cAAA;EACE,SAAA,CAAA,EAAA,MAAA;EAEA,SAAA,OAAA,EA5ES,aA4ET,CAAA,MAAA,CAAA;EACgB,WAAA,CAAA,IAAA,EAAA;IAAV,OAAA,EA3Ec,aA2Ed,CAAA,MAAA,CAAA;IAAd,CAAA,CAAA,EAAA,MAAA;EAAa,CAAA;yBAzDe,oCAEpB,cAAc,cAAc,UAAU,sBACpC,gBACR,QAAQ,cAAc,UAAU;;;;;;;;;;iBAmBrB,wBAAwB,qBAC/B,cAAc,cAAc,UAAU,gCAEpC,wBACR,cAAc,UAAU;;;;;;;;;;;;;;;;;;;;;;iBAyBX,6BAA6B,qBACpC,cAAc,cAAc,UAAU,qBACpC,uDAEA,wBACR,cAAc,UAAU"}
@@ -0,0 +1,166 @@
1
+ //#region src/search/rrf.ts
2
+ /**
3
+ * Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes
4
+ * top-ranked items dominate; higher `k` smooths the contribution of
5
+ * lower-ranked items. `k = 60` is the value popularised by Cormack,
6
+ * Clarke, and Büttcher (2009) and is the framework default.
7
+ *
8
+ * @stable
9
+ */
10
+ const RRF_DEFAULT_K = 60;
11
+ /**
12
+ * Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked
13
+ * lists (vector hits, FTS5 hits, optional entity boost) into a single
14
+ * fused ranking by summing `1 / (k + rank)` per list each item appears
15
+ * in.
16
+ *
17
+ * Properties (verified by the property-based test suite):
18
+ *
19
+ * - **Deterministic.** Identical input lists yield identical output.
20
+ * - **Stable under permutation of input lists.** The fusion is
21
+ * independent of the order in which the input lists are passed in.
22
+ * - **Tie-broken by stable record id.** Records with equal RRF
23
+ * scores keep their first-seen order.
24
+ *
25
+ * @stable
26
+ */
27
+ var RRFReranker = class {
28
+ id = "rrf";
29
+ k;
30
+ constructor(k = RRF_DEFAULT_K) {
31
+ if (!Number.isFinite(k) || k <= 0) throw new TypeError(`[graphorin/memory] RRFReranker: 'k' must be a positive finite number, got ${String(k)}.`);
32
+ this.k = k;
33
+ }
34
+ async rerank(query, lists, options = {}) {
35
+ if (options.signal?.aborted === true) throw new DOMException("RRFReranker aborted", "AbortError");
36
+ const fused = fuseRrf(lists, this.k, options.labels);
37
+ const topK = options.topK ?? 10;
38
+ return fused.slice(0, Math.max(0, topK));
39
+ }
40
+ };
41
+ /**
42
+ * Weighted-RRF reranker (X-2). Fuses parallel ranked lists through
43
+ * {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who
44
+ * has calibrated list reliability against labels (the P0-1 eval harness)
45
+ * can trust one retriever over another — e.g. up-weight dense vector
46
+ * hits over lexical FTS hits. `weights` is **positional**: aligned to the
47
+ * input `lists` order (the built-in hybrid search passes FTS first, then
48
+ * vector). At equal weights it is identical to {@link RRFReranker}; RRF
49
+ * stays the framework default.
50
+ *
51
+ * Like the RRF reranker it is deterministic and tie-broken by stable
52
+ * record id, and it preserves the upstream `signals` on each hit.
53
+ *
54
+ * @stable
55
+ */
56
+ var WeightedRRFReranker = class {
57
+ id = "weighted-rrf";
58
+ k;
59
+ weights;
60
+ constructor(args) {
61
+ const k = args.k ?? RRF_DEFAULT_K;
62
+ if (!Number.isFinite(k) || k <= 0) throw new TypeError(`[graphorin/memory] WeightedRRFReranker: 'k' must be a positive finite number, got ${String(k)}.`);
63
+ for (const w of args.weights) if (!Number.isFinite(w) || w < 0) throw new TypeError(`[graphorin/memory] WeightedRRFReranker: every weight must be a non-negative finite number, got ${String(w)}.`);
64
+ this.k = k;
65
+ this.weights = args.weights;
66
+ }
67
+ async rerank(query, lists, options = {}) {
68
+ if (options.signal?.aborted === true) throw new DOMException("WeightedRRFReranker aborted", "AbortError");
69
+ const fused = fuseWeighted(lists, this.weights, this.k, options.labels);
70
+ const topK = options.topK ?? 10;
71
+ return fused.slice(0, Math.max(0, topK));
72
+ }
73
+ };
74
+ /**
75
+ * Pure functional core of the RRF reranker — the equal-weight case of
76
+ * {@link fuseWeighted}. Exported separately so the test suite (and the
77
+ * property-based fuzzer) can exercise the math without the `Promise<…>`
78
+ * wrapping of the public surface.
79
+ *
80
+ * @stable
81
+ */
82
+ function fuseRrf(lists, k, labels) {
83
+ return fuseWeighted(lists, void 0, k, labels);
84
+ }
85
+ /**
86
+ * Weighted / convex generalization of {@link fuseRrf} (X-2). Each input
87
+ * list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused
88
+ * score instead of the flat `1 / (k + rank)`, so a caller who has
89
+ * calibrated list reliability against labels (the P0-1 eval harness) can
90
+ * trust one retriever over another. RRF stays the zero-tuning default: a
91
+ * `weights` of `undefined` (or all-`1`) is byte-for-byte identical to
92
+ * {@link fuseRrf}. A missing, non-finite, or negative entry falls back to
93
+ * the neutral weight `1`, so a partial / malformed `weights` never throws
94
+ * or poisons the ranking.
95
+ *
96
+ * Like {@link fuseRrf} the fusion is deterministic, tie-broken by
97
+ * first-seen order, and preserves any upstream `signals` (FTS `bm25`,
98
+ * vector similarity) the hits carried in — the `rrf` / `rrf.list_N`
99
+ * signals it records are the *weighted* contributions, so the fused
100
+ * `score` still equals their sum. Note that — unlike RRF — the result
101
+ * depends on input list *order*, because each weight is bound to a list
102
+ * position.
103
+ *
104
+ * @stable
105
+ */
106
+ function fuseWeighted(lists, weights, k, labels) {
107
+ const aggregates = /* @__PURE__ */ new Map();
108
+ let nextOrder = 0;
109
+ for (let listIdx = 0; listIdx < lists.length; listIdx++) {
110
+ const list = lists[listIdx] ?? [];
111
+ const weight = resolveWeight(weights, listIdx);
112
+ const signalKey = `rrf.${labels?.[listIdx] ?? `list_${listIdx}`}`;
113
+ for (let i = 0; i < list.length; i++) {
114
+ const hit = list[i];
115
+ if (hit === void 0) continue;
116
+ const contribution = weight / (k + (i + 1));
117
+ const id = hit.record.id;
118
+ const existing = aggregates.get(id);
119
+ if (existing === void 0) {
120
+ const signals = {
121
+ ...hit.signals ?? {},
122
+ rrf: contribution,
123
+ [signalKey]: contribution
124
+ };
125
+ aggregates.set(id, {
126
+ record: hit.record,
127
+ score: contribution,
128
+ signals,
129
+ firstSeenOrder: nextOrder++
130
+ });
131
+ } else {
132
+ existing.score += contribution;
133
+ existing.signals[signalKey] = contribution;
134
+ existing.signals.rrf = (existing.signals.rrf ?? 0) + contribution;
135
+ for (const [key, value] of Object.entries(hit.signals ?? {})) {
136
+ if (key in existing.signals) continue;
137
+ existing.signals[key] = value;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ const fused = [];
143
+ for (const a of aggregates.values()) fused.push({
144
+ record: a.record,
145
+ score: a.score,
146
+ signals: Object.freeze({ ...a.signals })
147
+ });
148
+ fused.sort((a, b) => {
149
+ if (b.score !== a.score) return b.score - a.score;
150
+ return (aggregates.get(a.record.id)?.firstSeenOrder ?? 0) - (aggregates.get(b.record.id)?.firstSeenOrder ?? 0);
151
+ });
152
+ return fused;
153
+ }
154
+ /**
155
+ * Resolve list `index`'s fusion weight, defaulting a missing / non-finite
156
+ * / negative entry to the neutral `1` (so an absent or malformed
157
+ * `weights` degrades to plain RRF rather than poisoning the ranking).
158
+ */
159
+ function resolveWeight(weights, index) {
160
+ const raw = weights?.[index];
161
+ return typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? raw : 1;
162
+ }
163
+
164
+ //#endregion
165
+ export { RRFReranker, RRF_DEFAULT_K, WeightedRRFReranker, fuseRrf, fuseWeighted };
166
+ //# sourceMappingURL=rrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rrf.js","names":["signals: Record<string, number>","fused: MemoryHit<TRecord>[]"],"sources":["../../src/search/rrf.ts"],"sourcesContent":["import type { MemoryHit, MemoryRecord } from '@graphorin/core';\nimport type { ReRanker, ReRankOptions } from './types.js';\n\n/**\n * Industry-standard Reciprocal Rank Fusion constant. Lower `k` makes\n * top-ranked items dominate; higher `k` smooths the contribution of\n * lower-ranked items. `k = 60` is the value popularised by Cormack,\n * Clarke, and Büttcher (2009) and is the framework default.\n *\n * @stable\n */\nexport const RRF_DEFAULT_K = 60;\n\n/**\n * Built-in Reciprocal Rank Fusion reranker. Combines multiple ranked\n * lists (vector hits, FTS5 hits, optional entity boost) into a single\n * fused ranking by summing `1 / (k + rank)` per list each item appears\n * in.\n *\n * Properties (verified by the property-based test suite):\n *\n * - **Deterministic.** Identical input lists yield identical output.\n * - **Stable under permutation of input lists.** The fusion is\n * independent of the order in which the input lists are passed in.\n * - **Tie-broken by stable record id.** Records with equal RRF\n * scores keep their first-seen order.\n *\n * @stable\n */\nexport class RRFReranker implements ReRanker {\n readonly id = 'rrf' as const;\n readonly k: number;\n\n constructor(k: number = RRF_DEFAULT_K) {\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] RRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n this.k = k;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('RRFReranker aborted', 'AbortError');\n }\n const fused = fuseRrf(lists, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Weighted-RRF reranker (X-2). Fuses parallel ranked lists through\n * {@link fuseWeighted}, applying a per-list `weights[i]` so a caller who\n * has calibrated list reliability against labels (the P0-1 eval harness)\n * can trust one retriever over another — e.g. up-weight dense vector\n * hits over lexical FTS hits. `weights` is **positional**: aligned to the\n * input `lists` order (the built-in hybrid search passes FTS first, then\n * vector). At equal weights it is identical to {@link RRFReranker}; RRF\n * stays the framework default.\n *\n * Like the RRF reranker it is deterministic and tie-broken by stable\n * record id, and it preserves the upstream `signals` on each hit.\n *\n * @stable\n */\nexport class WeightedRRFReranker implements ReRanker {\n readonly id = 'weighted-rrf' as const;\n readonly k: number;\n readonly weights: ReadonlyArray<number>;\n\n constructor(args: { weights: ReadonlyArray<number>; k?: number }) {\n const k = args.k ?? RRF_DEFAULT_K;\n if (!Number.isFinite(k) || k <= 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: 'k' must be a positive finite number, got ${String(k)}.`,\n );\n }\n for (const w of args.weights) {\n if (!Number.isFinite(w) || w < 0) {\n throw new TypeError(\n `[graphorin/memory] WeightedRRFReranker: every weight must be a non-negative finite number, got ${String(w)}.`,\n );\n }\n }\n this.k = k;\n this.weights = args.weights;\n }\n\n async rerank<TRecord extends MemoryRecord>(\n query: string,\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n options: ReRankOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<TRecord>>> {\n void query;\n if (options.signal?.aborted === true) {\n throw new DOMException('WeightedRRFReranker aborted', 'AbortError');\n }\n const fused = fuseWeighted(lists, this.weights, this.k, options.labels);\n const topK = options.topK ?? 10;\n return fused.slice(0, Math.max(0, topK));\n }\n}\n\n/**\n * Pure functional core of the RRF reranker — the equal-weight case of\n * {@link fuseWeighted}. Exported separately so the test suite (and the\n * property-based fuzzer) can exercise the math without the `Promise<…>`\n * wrapping of the public surface.\n *\n * @stable\n */\nexport function fuseRrf<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n return fuseWeighted(lists, undefined, k, labels);\n}\n\n/**\n * Weighted / convex generalization of {@link fuseRrf} (X-2). Each input\n * list `i` contributes `weights[i] · 1 / (k + rank)` to a record's fused\n * score instead of the flat `1 / (k + rank)`, so a caller who has\n * calibrated list reliability against labels (the P0-1 eval harness) can\n * trust one retriever over another. RRF stays the zero-tuning default: a\n * `weights` of `undefined` (or all-`1`) is byte-for-byte identical to\n * {@link fuseRrf}. A missing, non-finite, or negative entry falls back to\n * the neutral weight `1`, so a partial / malformed `weights` never throws\n * or poisons the ranking.\n *\n * Like {@link fuseRrf} the fusion is deterministic, tie-broken by\n * first-seen order, and preserves any upstream `signals` (FTS `bm25`,\n * vector similarity) the hits carried in — the `rrf` / `rrf.list_N`\n * signals it records are the *weighted* contributions, so the fused\n * `score` still equals their sum. Note that — unlike RRF — the result\n * depends on input list *order*, because each weight is bound to a list\n * position.\n *\n * @stable\n */\nexport function fuseWeighted<TRecord extends MemoryRecord>(\n lists: ReadonlyArray<ReadonlyArray<MemoryHit<TRecord>>>,\n weights: ReadonlyArray<number> | undefined,\n k: number,\n labels?: ReadonlyArray<string>,\n): ReadonlyArray<MemoryHit<TRecord>> {\n type Aggregate = {\n record: TRecord;\n score: number;\n signals: Record<string, number>;\n firstSeenOrder: number;\n };\n const aggregates = new Map<string, Aggregate>();\n let nextOrder = 0;\n for (let listIdx = 0; listIdx < lists.length; listIdx++) {\n const list = lists[listIdx] ?? [];\n const weight = resolveWeight(weights, listIdx);\n // MRET-13: explanation signals key by the caller's retriever-kind\n // label when supplied — `rrf.list_N` is an ephemeral position that\n // changes meaning whenever a leg (vector / HyDE / graph) is\n // conditionally absent from the fan-out.\n const signalKey = `rrf.${labels?.[listIdx] ?? `list_${listIdx}`}`;\n for (let i = 0; i < list.length; i++) {\n const hit = list[i];\n if (hit === undefined) continue;\n const rank = i + 1;\n const contribution = weight / (k + rank);\n const id = hit.record.id;\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const signals: Record<string, number> = {\n ...(hit.signals ?? {}),\n rrf: contribution,\n [signalKey]: contribution,\n };\n aggregates.set(id, {\n record: hit.record,\n score: contribution,\n signals,\n firstSeenOrder: nextOrder++,\n });\n } else {\n existing.score += contribution;\n existing.signals[signalKey] = contribution;\n existing.signals.rrf = (existing.signals.rrf ?? 0) + contribution;\n for (const [key, value] of Object.entries(hit.signals ?? {})) {\n if (key in existing.signals) continue;\n existing.signals[key] = value;\n }\n }\n }\n }\n const fused: MemoryHit<TRecord>[] = [];\n for (const a of aggregates.values()) {\n fused.push({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n });\n }\n fused.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n const orderA = aggregates.get(a.record.id)?.firstSeenOrder ?? 0;\n const orderB = aggregates.get(b.record.id)?.firstSeenOrder ?? 0;\n return orderA - orderB;\n });\n return fused;\n}\n\n/**\n * Resolve list `index`'s fusion weight, defaulting a missing / non-finite\n * / negative entry to the neutral `1` (so an absent or malformed\n * `weights` degrades to plain RRF rather than poisoning the ranking).\n */\nfunction resolveWeight(weights: ReadonlyArray<number> | undefined, index: number): number {\n const raw = weights?.[index];\n return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : 1;\n}\n"],"mappings":"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"}