@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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/conflict/index.d.ts +3 -0
- package/dist/conflict/index.js +7 -0
- package/dist/conflict/locale-packs/en.js +262 -0
- package/dist/conflict/locale-packs/en.js.map +1 -0
- package/dist/conflict/locale-packs/index.js +4 -0
- package/dist/conflict/locale-packs/types.d.ts +103 -0
- package/dist/conflict/locale-packs/types.d.ts.map +1 -0
- package/dist/conflict/locale-packs/types.js +55 -0
- package/dist/conflict/locale-packs/types.js.map +1 -0
- package/dist/conflict/pipeline.d.ts +35 -0
- package/dist/conflict/pipeline.d.ts.map +1 -0
- package/dist/conflict/pipeline.js +257 -0
- package/dist/conflict/pipeline.js.map +1 -0
- package/dist/conflict/stages/helpers.js +66 -0
- package/dist/conflict/stages/helpers.js.map +1 -0
- package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
- package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
- package/dist/conflict/types.d.ts +222 -0
- package/dist/conflict/types.d.ts.map +1 -0
- package/dist/conflict/types.js +50 -0
- package/dist/conflict/types.js.map +1 -0
- package/dist/consolidator/budget.d.ts +1 -0
- package/dist/consolidator/budget.js +214 -0
- package/dist/consolidator/budget.js.map +1 -0
- package/dist/consolidator/decay.d.ts +35 -0
- package/dist/consolidator/decay.d.ts.map +1 -0
- package/dist/consolidator/decay.js +91 -0
- package/dist/consolidator/decay.js.map +1 -0
- package/dist/consolidator/dlq.js +58 -0
- package/dist/consolidator/dlq.js.map +1 -0
- package/dist/consolidator/errors.d.ts +54 -0
- package/dist/consolidator/errors.d.ts.map +1 -0
- package/dist/consolidator/errors.js +67 -0
- package/dist/consolidator/errors.js.map +1 -0
- package/dist/consolidator/idempotency.d.ts +1 -0
- package/dist/consolidator/idempotency.js +22 -0
- package/dist/consolidator/idempotency.js.map +1 -0
- package/dist/consolidator/index.d.ts +34 -0
- package/dist/consolidator/index.d.ts.map +1 -0
- package/dist/consolidator/index.js +163 -0
- package/dist/consolidator/index.js.map +1 -0
- package/dist/consolidator/lock.d.ts +2 -0
- package/dist/consolidator/lock.js +89 -0
- package/dist/consolidator/lock.js.map +1 -0
- package/dist/consolidator/noise-filter.d.ts +2 -0
- package/dist/consolidator/noise-filter.js +173 -0
- package/dist/consolidator/noise-filter.js.map +1 -0
- package/dist/consolidator/phases/deep.d.ts +4 -0
- package/dist/consolidator/phases/deep.js +198 -0
- package/dist/consolidator/phases/deep.js.map +1 -0
- package/dist/consolidator/phases/induce.d.ts +154 -0
- package/dist/consolidator/phases/induce.d.ts.map +1 -0
- package/dist/consolidator/phases/induce.js +253 -0
- package/dist/consolidator/phases/induce.js.map +1 -0
- package/dist/consolidator/phases/light.d.ts +4 -0
- package/dist/consolidator/phases/light.js +99 -0
- package/dist/consolidator/phases/light.js.map +1 -0
- package/dist/consolidator/phases/reflect.js +287 -0
- package/dist/consolidator/phases/reflect.js.map +1 -0
- package/dist/consolidator/phases/standard.d.ts +8 -0
- package/dist/consolidator/phases/standard.js +499 -0
- package/dist/consolidator/phases/standard.js.map +1 -0
- package/dist/consolidator/reconcile.js +192 -0
- package/dist/consolidator/reconcile.js.map +1 -0
- package/dist/consolidator/runtime.d.ts +62 -0
- package/dist/consolidator/runtime.d.ts.map +1 -0
- package/dist/consolidator/runtime.js +580 -0
- package/dist/consolidator/runtime.js.map +1 -0
- package/dist/consolidator/scheduler.d.ts +111 -0
- package/dist/consolidator/scheduler.d.ts.map +1 -0
- package/dist/consolidator/scheduler.js +71 -0
- package/dist/consolidator/scheduler.js.map +1 -0
- package/dist/consolidator/triggers.d.ts +40 -0
- package/dist/consolidator/triggers.d.ts.map +1 -0
- package/dist/consolidator/triggers.js +94 -0
- package/dist/consolidator/triggers.js.map +1 -0
- package/dist/consolidator/types.d.ts +397 -0
- package/dist/consolidator/types.d.ts.map +1 -0
- package/dist/consolidator/types.js +117 -0
- package/dist/consolidator/types.js.map +1 -0
- package/dist/context-engine/annotations.d.ts +118 -0
- package/dist/context-engine/annotations.d.ts.map +1 -0
- package/dist/context-engine/annotations.js +102 -0
- package/dist/context-engine/annotations.js.map +1 -0
- package/dist/context-engine/auto-recall.d.ts +52 -0
- package/dist/context-engine/auto-recall.d.ts.map +1 -0
- package/dist/context-engine/auto-recall.js +41 -0
- package/dist/context-engine/auto-recall.js.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/clear-tool-results.js +97 -0
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
- package/dist/context-engine/compaction/compactor.d.ts +52 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
- package/dist/context-engine/compaction/compactor.js +196 -0
- package/dist/context-engine/compaction/compactor.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
- package/dist/context-engine/compaction/index.d.ts +9 -0
- package/dist/context-engine/compaction/index.js +9 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
- package/dist/context-engine/compaction/thresholds.d.ts +68 -0
- package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
- package/dist/context-engine/compaction/thresholds.js +63 -0
- package/dist/context-engine/compaction/thresholds.js.map +1 -0
- package/dist/context-engine/compaction/types.d.ts +202 -0
- package/dist/context-engine/compaction/types.d.ts.map +1 -0
- package/dist/context-engine/engine.d.ts +258 -0
- package/dist/context-engine/engine.d.ts.map +1 -0
- package/dist/context-engine/engine.js +424 -0
- package/dist/context-engine/engine.js.map +1 -0
- package/dist/context-engine/index.d.ts +68 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +24 -0
- package/dist/context-engine/locale-packs/en.d.ts +14 -0
- package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/en.js +87 -0
- package/dist/context-engine/locale-packs/en.js.map +1 -0
- package/dist/context-engine/locale-packs/index.js +4 -0
- package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
- package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/resolver.js +119 -0
- package/dist/context-engine/locale-packs/resolver.js.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +119 -0
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.js +24 -0
- package/dist/context-engine/locale-packs/types.js.map +1 -0
- package/dist/context-engine/metadata.d.ts +40 -0
- package/dist/context-engine/metadata.d.ts.map +1 -0
- package/dist/context-engine/metadata.js +66 -0
- package/dist/context-engine/metadata.js.map +1 -0
- package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
- package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
- package/dist/context-engine/preambles/inbound-en.js +23 -0
- package/dist/context-engine/preambles/inbound-en.js.map +1 -0
- package/dist/context-engine/privacy-filter.d.ts +108 -0
- package/dist/context-engine/privacy-filter.d.ts.map +1 -0
- package/dist/context-engine/privacy-filter.js +113 -0
- package/dist/context-engine/privacy-filter.js.map +1 -0
- package/dist/context-engine/templates/base-en.d.ts +19 -0
- package/dist/context-engine/templates/base-en.d.ts.map +1 -0
- package/dist/context-engine/templates/base-en.js +22 -0
- package/dist/context-engine/templates/base-en.js.map +1 -0
- package/dist/context-engine/templates/composer.d.ts +55 -0
- package/dist/context-engine/templates/composer.d.ts.map +1 -0
- package/dist/context-engine/templates/composer.js +51 -0
- package/dist/context-engine/templates/composer.js.map +1 -0
- package/dist/context-engine/token-budget.d.ts +99 -0
- package/dist/context-engine/token-budget.d.ts.map +1 -0
- package/dist/context-engine/token-budget.js +211 -0
- package/dist/context-engine/token-budget.js.map +1 -0
- package/dist/context-engine/token-counter.d.ts +54 -0
- package/dist/context-engine/token-counter.d.ts.map +1 -0
- package/dist/context-engine/token-counter.js +81 -0
- package/dist/context-engine/token-counter.js.map +1 -0
- package/dist/errors/index.d.ts +171 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +206 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/facade.d.ts +278 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +265 -0
- package/dist/facade.js.map +1 -0
- package/dist/graph/entity-resolver.d.ts +135 -0
- package/dist/graph/entity-resolver.d.ts.map +1 -0
- package/dist/graph/entity-resolver.js +202 -0
- package/dist/graph/entity-resolver.js.map +1 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/contextualize.d.ts +14 -0
- package/dist/internal/contextualize.d.ts.map +1 -0
- package/dist/internal/contextualize.js +110 -0
- package/dist/internal/contextualize.js.map +1 -0
- package/dist/internal/embedder-binding.js +50 -0
- package/dist/internal/embedder-binding.js.map +1 -0
- package/dist/internal/id.js +24 -0
- package/dist/internal/id.js.map +1 -0
- package/dist/internal/injection-heuristics.js +64 -0
- package/dist/internal/injection-heuristics.js.map +1 -0
- package/dist/internal/spans.js +35 -0
- package/dist/internal/spans.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +742 -0
- package/dist/internal/storage-adapter.d.ts.map +1 -0
- package/dist/migration/embedder-migration.d.ts +124 -0
- package/dist/migration/embedder-migration.d.ts.map +1 -0
- package/dist/migration/embedder-migration.js +109 -0
- package/dist/migration/embedder-migration.js.map +1 -0
- package/dist/migration/index.d.ts +2 -0
- package/dist/migration/index.js +3 -0
- package/dist/search/explain.d.ts +74 -0
- package/dist/search/explain.d.ts.map +1 -0
- package/dist/search/explain.js +50 -0
- package/dist/search/explain.js.map +1 -0
- package/dist/search/index.d.ts +6 -0
- package/dist/search/index.js +6 -0
- package/dist/search/iterative.d.ts +223 -0
- package/dist/search/iterative.d.ts.map +1 -0
- package/dist/search/iterative.js +330 -0
- package/dist/search/iterative.js.map +1 -0
- package/dist/search/query-transform.d.ts +112 -0
- package/dist/search/query-transform.d.ts.map +1 -0
- package/dist/search/query-transform.js +184 -0
- package/dist/search/query-transform.js.map +1 -0
- package/dist/search/rrf.d.ts +95 -0
- package/dist/search/rrf.d.ts.map +1 -0
- package/dist/search/rrf.js +166 -0
- package/dist/search/rrf.js.map +1 -0
- package/dist/search/types.d.ts +50 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.d.ts +142 -0
- package/dist/tiers/episodic-memory.d.ts.map +1 -0
- package/dist/tiers/episodic-memory.js +254 -0
- package/dist/tiers/episodic-memory.js.map +1 -0
- package/dist/tiers/index.d.ts +8 -0
- package/dist/tiers/index.js +9 -0
- package/dist/tiers/insight-memory.d.ts +71 -0
- package/dist/tiers/insight-memory.d.ts.map +1 -0
- package/dist/tiers/insight-memory.js +133 -0
- package/dist/tiers/insight-memory.js.map +1 -0
- package/dist/tiers/procedural-memory.d.ts +147 -0
- package/dist/tiers/procedural-memory.d.ts.map +1 -0
- package/dist/tiers/procedural-memory.js +267 -0
- package/dist/tiers/procedural-memory.js.map +1 -0
- package/dist/tiers/semantic-memory.d.ts +461 -0
- package/dist/tiers/semantic-memory.d.ts.map +1 -0
- package/dist/tiers/semantic-memory.js +631 -0
- package/dist/tiers/semantic-memory.js.map +1 -0
- package/dist/tiers/session-memory.d.ts +117 -0
- package/dist/tiers/session-memory.d.ts.map +1 -0
- package/dist/tiers/session-memory.js +163 -0
- package/dist/tiers/session-memory.js.map +1 -0
- package/dist/tiers/shared-memory.d.ts +29 -0
- package/dist/tiers/shared-memory.d.ts.map +1 -0
- package/dist/tiers/shared-memory.js +58 -0
- package/dist/tiers/shared-memory.js.map +1 -0
- package/dist/tiers/working-memory.d.ts +129 -0
- package/dist/tiers/working-memory.d.ts.map +1 -0
- package/dist/tiers/working-memory.js +229 -0
- package/dist/tiers/working-memory.js.map +1 -0
- package/dist/tools/block-tools.d.ts +98 -0
- package/dist/tools/block-tools.d.ts.map +1 -0
- package/dist/tools/block-tools.js +114 -0
- package/dist/tools/block-tools.js.map +1 -0
- package/dist/tools/fact-tools.d.ts +294 -0
- package/dist/tools/fact-tools.d.ts.map +1 -0
- package/dist/tools/fact-tools.js +293 -0
- package/dist/tools/fact-tools.js.map +1 -0
- package/dist/tools/index.d.ts +35 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/recall-tools.d.ts +209 -0
- package/dist/tools/recall-tools.d.ts.map +1 -0
- package/dist/tools/recall-tools.js +170 -0
- package/dist/tools/recall-tools.js.map +1 -0
- package/dist/tools/types.d.ts +40 -0
- package/dist/tools/types.d.ts.map +1 -0
- 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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"}
|