oh-my-second-brain 0.3.0 → 0.6.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/ACKNOWLEDGMENTS.md +8 -0
- package/CHANGELOG-assets.md +10 -0
- package/CHANGELOG-cli.md +11 -0
- package/CHANGELOG-kernel.md +31 -0
- package/CHANGELOG-mcp.md +12 -0
- package/CHANGELOG-vendors.md +6 -0
- package/CHANGELOG.md +35 -0
- package/assets/hermes-manifest.json +1 -1
- package/assets/skills/doctor/SKILL.md +2 -2
- package/assets/skills/search/SKILL.md +17 -1
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +13 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/host-commands.js +2 -1
- package/dist/cli/host-commands.js.map +1 -1
- package/dist/cli/oms.d.ts +1 -1
- package/dist/cli/oms.js +26 -3
- package/dist/cli/oms.js.map +1 -1
- package/dist/cli/semantic-args.js +102 -3
- package/dist/cli/semantic-args.js.map +1 -1
- package/dist/cli/semantic.js +30 -6
- package/dist/cli/semantic.js.map +1 -1
- package/dist/cli/setup-command.d.ts +16 -0
- package/dist/cli/setup-command.js +50 -1
- package/dist/cli/setup-command.js.map +1 -1
- package/dist/kernel/contracts/index.d.ts +144 -0
- package/dist/kernel/contracts/index.js +401 -0
- package/dist/kernel/contracts/index.js.map +1 -0
- package/dist/kernel/conventions/note-exclude.d.ts +14 -0
- package/dist/kernel/conventions/note-exclude.js +95 -0
- package/dist/kernel/conventions/note-exclude.js.map +1 -0
- package/dist/kernel/engine/assemble.d.ts +22 -1
- package/dist/kernel/engine/assemble.js +202 -19
- package/dist/kernel/engine/assemble.js.map +1 -1
- package/dist/kernel/engine/axes/store.d.ts +90 -0
- package/dist/kernel/engine/axes/store.js +422 -0
- package/dist/kernel/engine/axes/store.js.map +1 -0
- package/dist/kernel/engine/conventions/vault-lint.d.ts +7 -2
- package/dist/kernel/engine/conventions/vault-lint.js +7 -21
- package/dist/kernel/engine/conventions/vault-lint.js.map +1 -1
- package/dist/kernel/engine/embed/identity.d.ts +14 -4
- package/dist/kernel/engine/embed/identity.js +33 -1
- package/dist/kernel/engine/embed/identity.js.map +1 -1
- package/dist/kernel/engine/embed/model.d.ts +134 -0
- package/dist/kernel/engine/embed/model.js +302 -0
- package/dist/kernel/engine/embed/model.js.map +1 -0
- package/dist/kernel/engine/embed/provider.d.ts +36 -5
- package/dist/kernel/engine/embed/provider.js +240 -103
- package/dist/kernel/engine/embed/provider.js.map +1 -1
- package/dist/kernel/engine/embed/store.d.ts +5 -1
- package/dist/kernel/engine/embed/store.js +136 -26
- package/dist/kernel/engine/embed/store.js.map +1 -1
- package/dist/kernel/engine/embed/sync.d.ts +53 -0
- package/dist/kernel/engine/embed/sync.js +489 -59
- package/dist/kernel/engine/embed/sync.js.map +1 -1
- package/dist/kernel/engine/graph/builder.d.ts +9 -5
- package/dist/kernel/engine/graph/builder.js +225 -82
- package/dist/kernel/engine/graph/builder.js.map +1 -1
- package/dist/kernel/engine/graph/node.d.ts +49 -1
- package/dist/kernel/engine/graph/node.js +205 -0
- package/dist/kernel/engine/graph/node.js.map +1 -1
- package/dist/kernel/engine/mcp/facade.d.ts +21 -1
- package/dist/kernel/engine/mcp/facade.js +523 -28
- package/dist/kernel/engine/mcp/facade.js.map +1 -1
- package/dist/kernel/engine/mcp/query-mapper.d.ts +27 -2
- package/dist/kernel/engine/mcp/query-mapper.js +157 -12
- package/dist/kernel/engine/mcp/query-mapper.js.map +1 -1
- package/dist/kernel/engine/mcp/types.d.ts +64 -2
- package/dist/kernel/engine/retrieval/dispatcher.d.ts +22 -0
- package/dist/kernel/engine/retrieval/dispatcher.js +84 -8
- package/dist/kernel/engine/retrieval/dispatcher.js.map +1 -1
- package/dist/kernel/engine/retrieval/index.d.ts +2 -1
- package/dist/kernel/engine/retrieval/index.js +1 -1
- package/dist/kernel/engine/retrieval/index.js.map +1 -1
- package/dist/kernel/engine/retrieval/reranker.d.ts +82 -4
- package/dist/kernel/engine/retrieval/reranker.js +178 -3
- package/dist/kernel/engine/retrieval/reranker.js.map +1 -1
- package/dist/kernel/engine/retrieval/rrf.d.ts +10 -2
- package/dist/kernel/engine/retrieval/rrf.js +39 -16
- package/dist/kernel/engine/retrieval/rrf.js.map +1 -1
- package/dist/kernel/engine/tracer.d.ts +13 -0
- package/dist/kernel/engine/tracer.js +63 -6
- package/dist/kernel/engine/tracer.js.map +1 -1
- package/dist/kernel/graph/cache.js +174 -23
- package/dist/kernel/graph/cache.js.map +1 -1
- package/dist/kernel/index.d.ts +2 -0
- package/dist/kernel/index.js +2 -0
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/measurement/no-default-contract.d.ts +17 -0
- package/dist/kernel/measurement/no-default-contract.js +84 -0
- package/dist/kernel/measurement/no-default-contract.js.map +1 -0
- package/dist/kernel/searchbackend/engine-search-backend.js +93 -7
- package/dist/kernel/searchbackend/engine-search-backend.js.map +1 -1
- package/dist/kernel/searchbackend/search-backend.d.ts +11 -1
- package/dist/kernel/searchbackend/search-backend.js +66 -5
- package/dist/kernel/searchbackend/search-backend.js.map +1 -1
- package/dist/kernel/semantic/semantic-engine.js +31 -5
- package/dist/kernel/semantic/semantic-engine.js.map +1 -1
- package/dist/kernel/semantic/semantic-retrieve-args.d.ts +19 -2
- package/dist/kernel/semantic/semantic-retrieve-args.js +81 -7
- package/dist/kernel/semantic/semantic-retrieve-args.js.map +1 -1
- package/dist/kernel/update/update.d.ts +7 -0
- package/dist/kernel/update/update.js +106 -8
- package/dist/kernel/update/update.js.map +1 -1
- package/dist/mcp/server.js +84 -34
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -2
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
* every async step.
|
|
12
12
|
*/
|
|
13
13
|
import { fuseRRF } from "./rrf.js";
|
|
14
|
+
function assertFiniteVector(vector, expectedDimensions) {
|
|
15
|
+
if (!(vector instanceof Float32Array)) {
|
|
16
|
+
throw new Error("Embedding provider returned a vector that is not a Float32Array.");
|
|
17
|
+
}
|
|
18
|
+
if (vector.length !== expectedDimensions) {
|
|
19
|
+
throw new Error(`Embedding provider returned ${vector.length} dimensions; expected ${expectedDimensions}.`);
|
|
20
|
+
}
|
|
21
|
+
for (let index = 0; index < vector.length; index += 1) {
|
|
22
|
+
if (!Number.isFinite(vector[index])) {
|
|
23
|
+
throw new Error(`Embedding provider returned a non-finite vector value at index ${index}.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function embedQuery(provider, text) {
|
|
28
|
+
const queryProvider = provider;
|
|
29
|
+
return queryProvider.embedQuery?.(text) ?? provider.embed(text);
|
|
30
|
+
}
|
|
14
31
|
/** Create a new mutable cancel token. */
|
|
15
32
|
export function createCancelToken() {
|
|
16
33
|
let cancelled = false;
|
|
@@ -59,6 +76,26 @@ const PROVENANCE_BOOST = {
|
|
|
59
76
|
curated: 0.01, // deliberately imported / curated references
|
|
60
77
|
"external-raw": 0, // raw external content — no boost
|
|
61
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Apply the provenance signal without putting it on a different scale from
|
|
81
|
+
* RRF. RRF scores are bounded by the number of lists and `rrfK` (roughly
|
|
82
|
+
* 0.016 for one rank-1 list with k=60), so adding a raw 0.02 would let a
|
|
83
|
+
* low-ranked authored hit leap over a stronger multi-list result. A
|
|
84
|
+
* multiplicative adjustment keeps the signal monotonic and proportional to
|
|
85
|
+
* the fused score.
|
|
86
|
+
*/
|
|
87
|
+
export function applyProvenanceBoost(score, provenance) {
|
|
88
|
+
const boost = provenance === undefined ? 0 : PROVENANCE_BOOST[provenance];
|
|
89
|
+
return score * (1 + boost);
|
|
90
|
+
}
|
|
91
|
+
export const DISPATCHER_POLICIES = Object.freeze([
|
|
92
|
+
"boost-additive",
|
|
93
|
+
"boost-k-scale",
|
|
94
|
+
"boost-per-list",
|
|
95
|
+
"boost-zero",
|
|
96
|
+
]);
|
|
97
|
+
export const PREREGISTERED_ARM_POLICIES = Object.freeze(["boost-k-scale", "boost-per-list", "boost-zero"]);
|
|
98
|
+
export const DEFAULT_DISPATCHER_POLICY = "boost-additive";
|
|
62
99
|
// ---------------------------------------------------------------------------
|
|
63
100
|
// Per-type dispatch
|
|
64
101
|
// ---------------------------------------------------------------------------
|
|
@@ -70,13 +107,15 @@ async function dispatchOne(sub, deps, k, cancel, collection) {
|
|
|
70
107
|
: deps.store.queryLex(sub.query, k, collection)), cancel);
|
|
71
108
|
}
|
|
72
109
|
case "vec": {
|
|
73
|
-
const vec = await withRetry(() => deps.embed
|
|
110
|
+
const vec = await withRetry(() => embedQuery(deps.embed, sub.query), cancel);
|
|
111
|
+
assertFiniteVector(vec, deps.embed.dimensions);
|
|
74
112
|
return withRetry(() => Promise.resolve(collection === undefined ? deps.store.queryVec(vec, k) : deps.store.queryVec(vec, k, collection)), cancel);
|
|
75
113
|
}
|
|
76
114
|
case "hyde": {
|
|
77
115
|
const generator = deps.hydeGenerator ?? defaultHydeGenerator;
|
|
78
116
|
const hypoDoc = await withRetry(() => generator(sub.query), cancel);
|
|
79
|
-
const vec = await withRetry(() => deps.embed
|
|
117
|
+
const vec = await withRetry(() => embedQuery(deps.embed, hypoDoc), cancel);
|
|
118
|
+
assertFiniteVector(vec, deps.embed.dimensions);
|
|
80
119
|
return withRetry(() => Promise.resolve(collection === undefined ? deps.store.queryVec(vec, k) : deps.store.queryVec(vec, k, collection)), cancel);
|
|
81
120
|
}
|
|
82
121
|
case "graph": {
|
|
@@ -91,6 +130,22 @@ async function dispatchOne(sub, deps, k, cancel, collection) {
|
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Apply the per-list policy before RRF ranking. RRF consumes rank positions,
|
|
135
|
+
* so the weighted score must be used to establish each list's order rather
|
|
136
|
+
* than applied after fusion (which would be the k-scale policy).
|
|
137
|
+
*/
|
|
138
|
+
function rankListForPolicy(list, deps, policy) {
|
|
139
|
+
if (policy !== "boost-per-list")
|
|
140
|
+
return list;
|
|
141
|
+
return [...list].sort((left, right) => {
|
|
142
|
+
const leftScore = applyProvenanceBoost(left.score, deps.provenanceMap?.(left.docPath));
|
|
143
|
+
const rightScore = applyProvenanceBoost(right.score, deps.provenanceMap?.(right.docPath));
|
|
144
|
+
return rightScore - leftScore ||
|
|
145
|
+
left.docPath.localeCompare(right.docPath) ||
|
|
146
|
+
left.chunkOrdinal - right.chunkOrdinal;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
94
149
|
// ---------------------------------------------------------------------------
|
|
95
150
|
// Public API
|
|
96
151
|
// ---------------------------------------------------------------------------
|
|
@@ -106,16 +161,27 @@ async function dispatchOne(sub, deps, k, cancel, collection) {
|
|
|
106
161
|
export async function dispatch(subQueries, deps, k = 10, cancel, collection) {
|
|
107
162
|
const token = cancel ?? createCancelToken();
|
|
108
163
|
const rrfK = deps.rrfK ?? 60;
|
|
164
|
+
const policy = deps.policy ?? DEFAULT_DISPATCHER_POLICY;
|
|
165
|
+
if (!Number.isSafeInteger(k) || k < 0) {
|
|
166
|
+
throw new Error("Retrieval candidate count k must be a non-negative safe integer.");
|
|
167
|
+
}
|
|
168
|
+
if (!Number.isFinite(rrfK) || rrfK < 0) {
|
|
169
|
+
throw new Error("RRF smoothing constant k must be a finite non-negative number.");
|
|
170
|
+
}
|
|
171
|
+
if (!DISPATCHER_POLICIES.includes(policy)) {
|
|
172
|
+
throw new Error(`Unknown dispatcher policy: ${String(policy)}`);
|
|
173
|
+
}
|
|
109
174
|
if (subQueries.length === 0)
|
|
110
175
|
return [];
|
|
111
176
|
// Execute all sub-queries in parallel (each with its own retry envelope)
|
|
112
177
|
const rankedLists = await Promise.all(subQueries.map((sub) => dispatchOne(sub, deps, k, token, collection)));
|
|
178
|
+
const policyLists = rankedLists.map((list) => rankListForPolicy(list, deps, policy));
|
|
113
179
|
// Build per-type score index for the perTypeScores field
|
|
114
180
|
// key = "docPath\0chunkOrdinal" → type → max raw score from that type
|
|
115
181
|
const typeScores = new Map();
|
|
116
182
|
for (let i = 0; i < subQueries.length; i++) {
|
|
117
183
|
const type = subQueries[i].type;
|
|
118
|
-
const list =
|
|
184
|
+
const list = policyLists[i];
|
|
119
185
|
for (const hit of list) {
|
|
120
186
|
const key = `${hit.docPath}\x00${hit.chunkOrdinal}`;
|
|
121
187
|
const perType = typeScores.get(key) ?? new Map();
|
|
@@ -126,22 +192,32 @@ export async function dispatch(subQueries, deps, k = 10, cancel, collection) {
|
|
|
126
192
|
}
|
|
127
193
|
}
|
|
128
194
|
// Fuse with RRF
|
|
129
|
-
const fused = fuseRRF(
|
|
130
|
-
|
|
131
|
-
|
|
195
|
+
const fused = fuseRRF(policyLists, rrfK, {
|
|
196
|
+
preserveInputOrder: policy === "boost-per-list",
|
|
197
|
+
});
|
|
198
|
+
// Build final results with provenance boost and per-type breakdown.
|
|
199
|
+
const results = fused.map((hit) => {
|
|
132
200
|
const key = `${hit.docPath}\x00${hit.chunkOrdinal}`;
|
|
133
201
|
const provenance = deps.provenanceMap?.(hit.docPath);
|
|
134
|
-
const boost = provenance
|
|
202
|
+
const boost = provenance === undefined ? 0 : PROVENANCE_BOOST[provenance];
|
|
135
203
|
const perTypeMap = typeScores.get(key);
|
|
136
204
|
const perTypeScores = perTypeMap !== undefined && perTypeMap.size > 0
|
|
137
205
|
? Object.fromEntries(perTypeMap)
|
|
138
206
|
: undefined;
|
|
139
207
|
return {
|
|
140
208
|
docPath: hit.docPath,
|
|
141
|
-
score:
|
|
209
|
+
score: policy === "boost-additive"
|
|
210
|
+
? hit.score + boost
|
|
211
|
+
: policy === "boost-k-scale"
|
|
212
|
+
? applyProvenanceBoost(hit.score, provenance)
|
|
213
|
+
: hit.score,
|
|
142
214
|
provenance,
|
|
143
215
|
perTypeScores,
|
|
144
216
|
};
|
|
145
217
|
});
|
|
218
|
+
if (policy === "boost-additive")
|
|
219
|
+
return results;
|
|
220
|
+
return results.sort((a, b) => b.score - a.score ||
|
|
221
|
+
a.docPath.localeCompare(b.docPath));
|
|
146
222
|
}
|
|
147
223
|
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAWH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { fuseRRF } from "./rrf.js";
|
|
13
13
|
export type { Reranker } from "./reranker.js";
|
|
14
|
-
export { PassthroughReranker, passthroughReranker } from "./reranker.js";
|
|
14
|
+
export { createLlamaReranker, DEFAULT_RERANKER_CANDIDATE_CAP, LlamaReranker, PassthroughReranker, passthroughReranker, } from "./reranker.js";
|
|
15
|
+
export type { LlamaModelLoader, LlamaRerankerOptions, RankingContext, RankingModel, } from "./reranker.js";
|
|
15
16
|
export { dispatch, createCancelToken, } from "./dispatcher.js";
|
|
16
17
|
export type { CancelToken, DispatcherDeps, HydeGenerator } from "./dispatcher.js";
|
|
17
18
|
import type { RetrievalResult } from "../types.js";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - DispatcherDeps — dependency-injection bag for backends
|
|
11
11
|
*/
|
|
12
12
|
export { fuseRRF } from "./rrf.js";
|
|
13
|
-
export { PassthroughReranker, passthroughReranker } from "./reranker.js";
|
|
13
|
+
export { createLlamaReranker, DEFAULT_RERANKER_CANDIDATE_CAP, LlamaReranker, PassthroughReranker, passthroughReranker, } from "./reranker.js";
|
|
14
14
|
export { dispatch, createCancelToken, } from "./dispatcher.js";
|
|
15
15
|
import { dispatch } from "./dispatcher.js";
|
|
16
16
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EACL,mBAAmB,EACnB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAOvB,OAAO,EACL,QAAQ,EACR,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAkC3C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAqB;IAClD,qDAAqD;IACrD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAEjG,2CAA2C;IAC3C,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,OAAO,CAAC;IAEnC,2EAA2E;IAC3E,MAAM,IAAI,GAAgB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,YAAY,EAAE,CAAC;QACf,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC,CAAC,CAAC;IAEJ,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAE9D,wEAAwE;IACxE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ;SACZ,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACzC,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAA4B,CAAC;IACjE,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAwB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Cross-encoder reranker hook.
|
|
3
3
|
*
|
|
4
4
|
* This module defines the Reranker interface and ships a passthrough (no-op)
|
|
5
|
-
* default so the precision-mode pipeline is wired end-to-end
|
|
6
|
-
* requiring a loaded reranker model.
|
|
5
|
+
* default so the precision-mode pipeline is wired end-to-end without
|
|
6
|
+
* requiring a loaded reranker model. The node-llama-cpp implementation below
|
|
7
|
+
* is explicitly opt-in and lazy: constructing it never loads a model or
|
|
8
|
+
* creates a ranking context.
|
|
7
9
|
*
|
|
8
10
|
* Intended production implementations (inject at the integrate phase):
|
|
9
11
|
* - bge-reranker-v2-m3 (BAAI, Apache-2.0) — strong multilingual reranker,
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
*
|
|
14
16
|
* Wire a real impl by passing it to retrieve() as `opts.reranker`.
|
|
15
17
|
*/
|
|
18
|
+
import type { LlamaRankingContextOptions } from "node-llama-cpp";
|
|
16
19
|
import type { ScoredHit } from "../types.js";
|
|
17
20
|
/**
|
|
18
21
|
* Opt-in cross-encoder reranker hook.
|
|
@@ -31,8 +34,81 @@ export interface Reranker {
|
|
|
31
34
|
* @returns A new list sorted descending by the reranker's cross-encoder score.
|
|
32
35
|
* The returned list may be shorter than `hits` (e.g. top-k precision cut).
|
|
33
36
|
*/
|
|
34
|
-
rerank(query: string, hits: ScoredHit[]
|
|
37
|
+
rerank(query: string, hits: ScoredHit[], cancel?: {
|
|
38
|
+
readonly cancelled: boolean;
|
|
39
|
+
}): Promise<ScoredHit[]>;
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Narrow seam around node-llama-cpp used by the lazy implementation.
|
|
43
|
+
*
|
|
44
|
+
* Keeping the seam injectable makes the lifecycle contract testable without
|
|
45
|
+
* downloading or loading a model. Production uses the default dynamic loader.
|
|
46
|
+
*/
|
|
47
|
+
export interface RankingContext {
|
|
48
|
+
rankAll(query: string, documents: string[]): Promise<readonly number[]>;
|
|
49
|
+
dispose(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export interface RankingModel {
|
|
52
|
+
createRankingContext(options?: LlamaRankingContextOptions): Promise<RankingContext>;
|
|
53
|
+
dispose(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export type LlamaModelLoader = (modelPath: string) => Promise<RankingModel>;
|
|
56
|
+
/** Conservative candidate budget; reranking is opt-in and default-off. */
|
|
57
|
+
export declare const DEFAULT_RERANKER_CANDIDATE_CAP = 50;
|
|
58
|
+
/**
|
|
59
|
+
* Options for {@link createLlamaReranker}.
|
|
60
|
+
*
|
|
61
|
+
* `idleUnloadMs` is disabled by default (`0`). Enabling it is useful for
|
|
62
|
+
* memory-constrained hosts, but the model is otherwise retained after its
|
|
63
|
+
* first requested rerank so repeated opt-in calls do not reload native state.
|
|
64
|
+
*/
|
|
65
|
+
export interface LlamaRerankerOptions {
|
|
66
|
+
readonly modelPath: string;
|
|
67
|
+
readonly candidateCap?: number;
|
|
68
|
+
readonly idleUnloadMs?: number;
|
|
69
|
+
readonly contextOptions?: LlamaRankingContextOptions;
|
|
70
|
+
/** Test seam; production callers should omit this. */
|
|
71
|
+
readonly loadModel?: LlamaModelLoader;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A lazy node-llama-cpp cross-encoder reranker.
|
|
75
|
+
*
|
|
76
|
+
* The model and `LlamaRankingContext` are created only by the first
|
|
77
|
+
* `rerank()` call. Only the first `candidateCap` fused hits are sent to the
|
|
78
|
+
* cross-encoder; hits beyond that safety budget are intentionally omitted.
|
|
79
|
+
* Scores are replaced with the cross-encoder scores and returned descending.
|
|
80
|
+
*/
|
|
81
|
+
export declare class LlamaReranker implements Reranker {
|
|
82
|
+
private readonly modelPath;
|
|
83
|
+
private readonly candidateCap;
|
|
84
|
+
private readonly idleUnloadMs;
|
|
85
|
+
private readonly contextOptions;
|
|
86
|
+
private readonly loadModel;
|
|
87
|
+
private model;
|
|
88
|
+
private context;
|
|
89
|
+
private loadPromise;
|
|
90
|
+
private idleTimer;
|
|
91
|
+
private activeCalls;
|
|
92
|
+
private activeDone;
|
|
93
|
+
private resolveActiveDone;
|
|
94
|
+
private disposed;
|
|
95
|
+
private disposePromise;
|
|
96
|
+
constructor(options: LlamaRerankerOptions);
|
|
97
|
+
private clearIdleTimer;
|
|
98
|
+
private scheduleIdleUnload;
|
|
99
|
+
private ensureContext;
|
|
100
|
+
private unload;
|
|
101
|
+
/** Release model/context resources; safe before the first rerank call. */
|
|
102
|
+
dispose(): Promise<void>;
|
|
103
|
+
rerank(query: string, hits: ScoredHit[], cancel?: {
|
|
104
|
+
readonly cancelled: boolean;
|
|
105
|
+
}): Promise<ScoredHit[]>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Construct a lazy reranker. No model import, native load, or context
|
|
109
|
+
* creation occurs until the returned reranker's `rerank()` is requested.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createLlamaReranker(options: LlamaRerankerOptions): LlamaReranker;
|
|
36
112
|
/**
|
|
37
113
|
* Passthrough (no-op) reranker — returns hits unchanged.
|
|
38
114
|
*
|
|
@@ -40,7 +116,9 @@ export interface Reranker {
|
|
|
40
116
|
* is available or when reranking overhead is not desired.
|
|
41
117
|
*/
|
|
42
118
|
export declare class PassthroughReranker implements Reranker {
|
|
43
|
-
rerank(_query: string, hits: ScoredHit[]
|
|
119
|
+
rerank(_query: string, hits: ScoredHit[], cancel?: {
|
|
120
|
+
readonly cancelled: boolean;
|
|
121
|
+
}): Promise<ScoredHit[]>;
|
|
44
122
|
}
|
|
45
123
|
/** Shared singleton passthrough instance — safe to reuse across calls. */
|
|
46
124
|
export declare const passthroughReranker: Reranker;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Cross-encoder reranker hook.
|
|
3
3
|
*
|
|
4
4
|
* This module defines the Reranker interface and ships a passthrough (no-op)
|
|
5
|
-
* default so the precision-mode pipeline is wired end-to-end
|
|
6
|
-
* requiring a loaded reranker model.
|
|
5
|
+
* default so the precision-mode pipeline is wired end-to-end without
|
|
6
|
+
* requiring a loaded reranker model. The node-llama-cpp implementation below
|
|
7
|
+
* is explicitly opt-in and lazy: constructing it never loads a model or
|
|
8
|
+
* creates a ranking context.
|
|
7
9
|
*
|
|
8
10
|
* Intended production implementations (inject at the integrate phase):
|
|
9
11
|
* - bge-reranker-v2-m3 (BAAI, Apache-2.0) — strong multilingual reranker,
|
|
@@ -13,6 +15,177 @@
|
|
|
13
15
|
*
|
|
14
16
|
* Wire a real impl by passing it to retrieve() as `opts.reranker`.
|
|
15
17
|
*/
|
|
18
|
+
/** Conservative candidate budget; reranking is opt-in and default-off. */
|
|
19
|
+
export const DEFAULT_RERANKER_CANDIDATE_CAP = 50;
|
|
20
|
+
async function loadLlamaModel(modelPath) {
|
|
21
|
+
const { getLlama } = await import("node-llama-cpp");
|
|
22
|
+
const llama = await getLlama();
|
|
23
|
+
return llama.loadModel({ modelPath });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A lazy node-llama-cpp cross-encoder reranker.
|
|
27
|
+
*
|
|
28
|
+
* The model and `LlamaRankingContext` are created only by the first
|
|
29
|
+
* `rerank()` call. Only the first `candidateCap` fused hits are sent to the
|
|
30
|
+
* cross-encoder; hits beyond that safety budget are intentionally omitted.
|
|
31
|
+
* Scores are replaced with the cross-encoder scores and returned descending.
|
|
32
|
+
*/
|
|
33
|
+
export class LlamaReranker {
|
|
34
|
+
modelPath;
|
|
35
|
+
candidateCap;
|
|
36
|
+
idleUnloadMs;
|
|
37
|
+
contextOptions;
|
|
38
|
+
loadModel;
|
|
39
|
+
model = null;
|
|
40
|
+
context = null;
|
|
41
|
+
loadPromise = null;
|
|
42
|
+
idleTimer = null;
|
|
43
|
+
activeCalls = 0;
|
|
44
|
+
activeDone = null;
|
|
45
|
+
resolveActiveDone = null;
|
|
46
|
+
disposed = false;
|
|
47
|
+
disposePromise = null;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
const candidateCap = options.candidateCap ?? DEFAULT_RERANKER_CANDIDATE_CAP;
|
|
50
|
+
if (!Number.isSafeInteger(candidateCap) || candidateCap < 1) {
|
|
51
|
+
throw new Error("reranker candidateCap must be a positive safe integer.");
|
|
52
|
+
}
|
|
53
|
+
const idleUnloadMs = options.idleUnloadMs ?? 0;
|
|
54
|
+
if (!Number.isFinite(idleUnloadMs) || idleUnloadMs < 0) {
|
|
55
|
+
throw new Error("reranker idleUnloadMs must be a non-negative number.");
|
|
56
|
+
}
|
|
57
|
+
if (options.modelPath.trim().length === 0) {
|
|
58
|
+
throw new Error("reranker modelPath must be non-empty.");
|
|
59
|
+
}
|
|
60
|
+
this.modelPath = options.modelPath;
|
|
61
|
+
this.candidateCap = candidateCap;
|
|
62
|
+
this.idleUnloadMs = idleUnloadMs;
|
|
63
|
+
this.contextOptions = options.contextOptions;
|
|
64
|
+
this.loadModel = options.loadModel ?? loadLlamaModel;
|
|
65
|
+
}
|
|
66
|
+
clearIdleTimer() {
|
|
67
|
+
if (this.idleTimer !== null) {
|
|
68
|
+
clearTimeout(this.idleTimer);
|
|
69
|
+
this.idleTimer = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scheduleIdleUnload() {
|
|
73
|
+
this.clearIdleTimer();
|
|
74
|
+
if (this.disposed || this.idleUnloadMs <= 0 || this.context === null)
|
|
75
|
+
return;
|
|
76
|
+
this.idleTimer = setTimeout(() => {
|
|
77
|
+
this.idleTimer = null;
|
|
78
|
+
if (this.activeCalls > 0) {
|
|
79
|
+
this.scheduleIdleUnload();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void this.unload().catch(() => undefined);
|
|
83
|
+
}, this.idleUnloadMs);
|
|
84
|
+
}
|
|
85
|
+
async ensureContext() {
|
|
86
|
+
if (this.disposed)
|
|
87
|
+
throw new Error("Reranker has been disposed.");
|
|
88
|
+
if (this.context !== null)
|
|
89
|
+
return this.context;
|
|
90
|
+
if (this.loadPromise !== null)
|
|
91
|
+
return this.loadPromise;
|
|
92
|
+
this.loadPromise = (async () => {
|
|
93
|
+
const model = await this.loadModel(this.modelPath);
|
|
94
|
+
this.model = model;
|
|
95
|
+
try {
|
|
96
|
+
const context = await model.createRankingContext(this.contextOptions);
|
|
97
|
+
this.context = context;
|
|
98
|
+
return context;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.model = null;
|
|
102
|
+
await model.dispose().catch(() => undefined);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
})().finally(() => {
|
|
106
|
+
this.loadPromise = null;
|
|
107
|
+
});
|
|
108
|
+
return this.loadPromise;
|
|
109
|
+
}
|
|
110
|
+
async unload() {
|
|
111
|
+
this.clearIdleTimer();
|
|
112
|
+
const context = this.context;
|
|
113
|
+
const model = this.model;
|
|
114
|
+
this.context = null;
|
|
115
|
+
this.model = null;
|
|
116
|
+
if (context !== null)
|
|
117
|
+
await context.dispose().catch(() => undefined);
|
|
118
|
+
if (model !== null)
|
|
119
|
+
await model.dispose().catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
/** Release model/context resources; safe before the first rerank call. */
|
|
122
|
+
async dispose() {
|
|
123
|
+
if (this.disposePromise !== null) {
|
|
124
|
+
return this.disposePromise;
|
|
125
|
+
}
|
|
126
|
+
this.disposed = true;
|
|
127
|
+
this.clearIdleTimer();
|
|
128
|
+
this.disposePromise = (async () => {
|
|
129
|
+
if (this.loadPromise !== null) {
|
|
130
|
+
await this.loadPromise.catch(() => undefined);
|
|
131
|
+
}
|
|
132
|
+
// Context disposal must never race a native rankAll() call. Calls
|
|
133
|
+
// already admitted before dispose() drain before unloading resources.
|
|
134
|
+
if (this.activeDone !== null)
|
|
135
|
+
await this.activeDone;
|
|
136
|
+
await this.unload();
|
|
137
|
+
})();
|
|
138
|
+
return this.disposePromise;
|
|
139
|
+
}
|
|
140
|
+
async rerank(query, hits, cancel) {
|
|
141
|
+
if (hits.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
if (this.disposed)
|
|
144
|
+
throw new Error("Reranker has been disposed.");
|
|
145
|
+
if (cancel?.cancelled)
|
|
146
|
+
throw new Error("Retrieval cancelled");
|
|
147
|
+
const candidates = hits.slice(0, this.candidateCap);
|
|
148
|
+
this.activeCalls += 1;
|
|
149
|
+
if (this.activeCalls === 1) {
|
|
150
|
+
this.activeDone = new Promise((resolve) => {
|
|
151
|
+
this.resolveActiveDone = resolve;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const context = await this.ensureContext();
|
|
156
|
+
if (cancel?.cancelled)
|
|
157
|
+
throw new Error("Retrieval cancelled");
|
|
158
|
+
const scores = await context.rankAll(query, candidates.map((hit) => hit.docPath));
|
|
159
|
+
if (cancel?.cancelled)
|
|
160
|
+
throw new Error("Retrieval cancelled");
|
|
161
|
+
if (scores.length !== candidates.length) {
|
|
162
|
+
throw new Error(`reranker returned ${scores.length} scores for ${candidates.length} candidates.`);
|
|
163
|
+
}
|
|
164
|
+
if (scores.some((score) => !Number.isFinite(score))) {
|
|
165
|
+
throw new Error("reranker returned a non-finite score.");
|
|
166
|
+
}
|
|
167
|
+
return candidates
|
|
168
|
+
.map((hit, index) => ({ ...hit, score: scores[index] }))
|
|
169
|
+
.sort((left, right) => right.score - left.score);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
this.activeCalls -= 1;
|
|
173
|
+
if (this.activeCalls === 0) {
|
|
174
|
+
this.resolveActiveDone?.();
|
|
175
|
+
this.resolveActiveDone = null;
|
|
176
|
+
this.activeDone = null;
|
|
177
|
+
}
|
|
178
|
+
this.scheduleIdleUnload();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Construct a lazy reranker. No model import, native load, or context
|
|
184
|
+
* creation occurs until the returned reranker's `rerank()` is requested.
|
|
185
|
+
*/
|
|
186
|
+
export function createLlamaReranker(options) {
|
|
187
|
+
return new LlamaReranker(options);
|
|
188
|
+
}
|
|
16
189
|
/**
|
|
17
190
|
* Passthrough (no-op) reranker — returns hits unchanged.
|
|
18
191
|
*
|
|
@@ -20,7 +193,9 @@
|
|
|
20
193
|
* is available or when reranking overhead is not desired.
|
|
21
194
|
*/
|
|
22
195
|
export class PassthroughReranker {
|
|
23
|
-
async rerank(_query, hits) {
|
|
196
|
+
async rerank(_query, hits, cancel) {
|
|
197
|
+
if (cancel?.cancelled)
|
|
198
|
+
throw new Error("Retrieval cancelled");
|
|
24
199
|
return hits;
|
|
25
200
|
}
|
|
26
201
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reranker.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/reranker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"reranker.js","sourceRoot":"","sources":["../../../../src/kernel/engine/retrieval/reranker.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,10 +14,18 @@ import type { ScoredHit } from "../types.js";
|
|
|
14
14
|
* Fuse multiple ranked lists into a single list using Reciprocal Rank Fusion.
|
|
15
15
|
*
|
|
16
16
|
* @param rankedLists - Per-modality ranked hit lists. Each inner list need not be
|
|
17
|
-
* pre-sorted
|
|
17
|
+
* pre-sorted unless `preserveInputOrder` is enabled.
|
|
18
18
|
* @param k - RRF smoothing constant (default 60). Higher values reduce the impact
|
|
19
19
|
* of rank position; typical range is 50–70.
|
|
20
|
+
* @param options - Ranking behavior for each input list.
|
|
20
21
|
* @returns Fused list sorted descending by RRF score. Ties are broken
|
|
21
22
|
* lexicographically by the composite key `"docPath\0chunkOrdinal"`.
|
|
22
23
|
*/
|
|
23
|
-
export
|
|
24
|
+
export interface FuseRRFOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Treat each list as already ranked and use its supplied order. This is
|
|
27
|
+
* required when a caller has established rank with a non-score policy.
|
|
28
|
+
*/
|
|
29
|
+
preserveInputOrder?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function fuseRRF(rankedLists: ScoredHit[][], k?: number, options?: FuseRRFOptions): ScoredHit[];
|
|
@@ -9,17 +9,38 @@
|
|
|
9
9
|
* where rank_i(d) is the 1-based position of document d in list i.
|
|
10
10
|
* Documents absent from a list contribute 0 for that list.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
function validateRrfInputs(rankedLists, k, options) {
|
|
13
|
+
if (!Array.isArray(rankedLists)) {
|
|
14
|
+
throw new Error("RRF ranked lists must be an array.");
|
|
15
|
+
}
|
|
16
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
17
|
+
throw new Error("RRF options must be an object.");
|
|
18
|
+
}
|
|
19
|
+
if (!Number.isFinite(k) || k < 0) {
|
|
20
|
+
throw new Error("RRF smoothing constant k must be a finite non-negative number.");
|
|
21
|
+
}
|
|
22
|
+
if (typeof options.preserveInputOrder !== "undefined" && typeof options.preserveInputOrder !== "boolean") {
|
|
23
|
+
throw new Error("RRF preserveInputOrder must be a boolean.");
|
|
24
|
+
}
|
|
25
|
+
for (const list of rankedLists) {
|
|
26
|
+
if (!Array.isArray(list)) {
|
|
27
|
+
throw new Error("RRF ranked lists must be arrays of scored hits.");
|
|
28
|
+
}
|
|
29
|
+
for (const hit of list) {
|
|
30
|
+
if (hit === null ||
|
|
31
|
+
typeof hit !== "object" ||
|
|
32
|
+
typeof hit.docPath !== "string" ||
|
|
33
|
+
hit.docPath.trim().length === 0 ||
|
|
34
|
+
!Number.isSafeInteger(hit.chunkOrdinal) ||
|
|
35
|
+
hit.chunkOrdinal < 0 ||
|
|
36
|
+
!Number.isFinite(hit.score)) {
|
|
37
|
+
throw new Error("RRF scored hits must have a non-empty path, non-negative ordinal, and finite score.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function fuseRRF(rankedLists, k = 60, options = {}) {
|
|
43
|
+
validateRrfInputs(rankedLists, k, options);
|
|
23
44
|
if (rankedLists.length === 0)
|
|
24
45
|
return [];
|
|
25
46
|
// key → { accumulated RRF score, original hit metadata }
|
|
@@ -27,11 +48,13 @@ export function fuseRRF(rankedLists, k = 60) {
|
|
|
27
48
|
for (const list of rankedLists) {
|
|
28
49
|
if (list.length === 0)
|
|
29
50
|
continue;
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
const sorted =
|
|
33
|
-
|
|
34
|
-
a.
|
|
51
|
+
// Dispatch policies may establish rank independently of raw score. In that
|
|
52
|
+
// case, preserve the caller's order rather than replacing it here.
|
|
53
|
+
const sorted = options.preserveInputOrder
|
|
54
|
+
? list
|
|
55
|
+
: [...list].sort((a, b) => b.score - a.score ||
|
|
56
|
+
a.docPath.localeCompare(b.docPath) ||
|
|
57
|
+
a.chunkOrdinal - b.chunkOrdinal);
|
|
35
58
|
for (let idx = 0; idx < sorted.length; idx++) {
|
|
36
59
|
const hit = sorted[idx];
|
|
37
60
|
const key = `${hit.docPath}\x00${hit.chunkOrdinal}`;
|