@hraness/wordcell 0.21.3 → 0.22.1
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/README.md +170 -64
- package/dist/authoring.js +5 -3
- package/dist/{authority-4sp9d552.js → authority-06eqhqam.js} +57 -12
- package/dist/benchmark.js +4 -4
- package/dist/capture.js +6 -5
- package/dist/cli.js +39 -33
- package/dist/clip/acquire.js +3 -2
- package/dist/clip/cli.js +6 -5
- package/dist/clip/doctor.js +4 -3
- package/dist/clip/jobs.js +3 -3
- package/dist/clip/refresh.js +1 -1
- package/dist/evaluation-builder.js +20 -19
- package/dist/evaluation-kb.js +16 -15
- package/dist/graph-authority.js +5 -5
- package/dist/graph-percolation.js +7 -7
- package/dist/graph.js +1 -1
- package/dist/{index-pbr6rv47.js → index-1k595z6v.js} +6 -6
- package/dist/{index-0k2x4nn9.js → index-233z9wmn.js} +17 -5
- package/dist/index-3agn8scn.js +306 -0
- package/dist/index-4knsp9qj.js +34 -0
- package/dist/index-66pshdtx.js +341 -0
- package/dist/{index-59ssmqqm.js → index-76dbn6pe.js} +1 -1
- package/dist/{index-0rfcqab6.js → index-810nfzdj.js} +617 -66
- package/dist/index-9rf81m0p.js +368 -0
- package/dist/{index-51bap0mq.js → index-bqtqeak3.js} +10 -10
- package/dist/{index-9zcpdbjg.js → index-de2w8crk.js} +13 -13
- package/dist/{index-bgfzwt4h.js → index-djrcc1yf.js} +6 -6
- package/dist/{index-6sw24nvv.js → index-ecqpgr5y.js} +53 -2
- package/dist/{index-van48n3g.js → index-fc4dr114.js} +4 -2
- package/dist/{index-jzcws0zn.js → index-g5vsqmdy.js} +27 -57
- package/dist/index-hmw17zaa.js +1985 -0
- package/dist/index-j70m75wd.js +217 -0
- package/dist/{index-nd6nynv2.js → index-jk1sgx36.js} +1 -1
- package/dist/{index-3ehtt59c.js → index-n386evxj.js} +1 -1
- package/dist/{index-vdw5yjfa.js → index-q7tbg07z.js} +110 -20
- package/dist/{index-ekpwvbra.js → index-qbssx940.js} +1 -2
- package/dist/{index-115b07ap.js → index-rr9kzq4n.js} +5 -5
- package/dist/{index-11621h23.js → index-tf4wzjew.js} +1 -1
- package/dist/{index-why54q5p.js → index-vnybgywh.js} +4 -4
- package/dist/{index-bcfn9xah.js → index-xv80vzfp.js} +7 -7
- package/dist/index.js +289 -95
- package/dist/percolate.js +2 -2
- package/dist/portfolio.js +16 -15
- package/dist/publish-model.js +36 -0
- package/dist/publish-reader/reader.css +1042 -0
- package/dist/publish-reader/reader.js +1534 -0
- package/dist/publish-reader/theme.js +92 -0
- package/dist/publish-search.js +52 -0
- package/dist/publish.js +36 -0
- package/dist/rerank-typesafe.js +13 -0
- package/dist/rerank.js +14 -0
- package/dist/sdk.js +16 -13
- package/dist/search.js +3 -3
- package/dist/semantic.js +5 -5
- package/dist/{support-vm4mkw0k.js → support-1rh2d7mt.js} +38 -32
- package/dist/workflows/decision-context.js +15 -14
- package/dist/workflows/index.js +15 -14
- package/package.json +65 -6
- package/skills/wordcell/AGENTS.md +1 -0
- package/skills/wordcell/SKILL.md +2 -1
- package/skills/wordcell/agents/openai.yaml +1 -1
- package/skills/wordcell/references/publish.md +127 -0
- package/skills/wordcell/references/query.md +22 -0
- package/skills/wordcell/references/refresh.md +14 -0
- package/src/authoring-model.ts +29 -0
- package/src/authoring-program.ts +38 -3
- package/src/authoring.ts +17 -3
- package/src/cli-program.ts +441 -12
- package/src/graph.ts +1 -1
- package/src/index.ts +4 -0
- package/src/oh/authority.ts +14 -5
- package/src/oh/canonical-rust.ts +127 -0
- package/src/oh/projection-rust.ts +63 -0
- package/src/oh-adoption.ts +7 -1
- package/src/publish-graph.ts +0 -0
- package/src/publish-index.ts +195 -0
- package/src/publish-markdown.ts +617 -0
- package/src/publish-model.ts +599 -0
- package/src/publish-nav.ts +204 -0
- package/src/publish-pages.ts +382 -0
- package/src/publish-reader/reader.css +1042 -0
- package/src/publish-reader/reader.ts +1017 -0
- package/src/publish-reader/theme.ts +97 -0
- package/src/publish-search.ts +418 -0
- package/src/publish-select.ts +419 -0
- package/src/publish-theme.ts +106 -0
- package/src/publish.ts +904 -0
- package/src/rerank-credentials.ts +56 -0
- package/src/rerank-typesafe.ts +472 -0
- package/src/rerank.ts +389 -0
- package/src/sdk.ts +157 -6
- package/src/serve.ts +180 -0
- package/src/vault.ts +23 -9
package/src/rerank.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
export const MAX_RERANK_CANDIDATES = 25;
|
|
2
|
+
export const MAX_RERANK_SNIPPET_BYTES = 512;
|
|
3
|
+
export const MAX_RERANK_STATE_BYTES = 24 * 1_024;
|
|
4
|
+
|
|
5
|
+
export type SearchRerankCandidate = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
/** One-based position in the fused baseline order. */
|
|
8
|
+
readonly baselineRank: number;
|
|
9
|
+
/** Baseline fused score. Engines must not treat it as a probability. */
|
|
10
|
+
readonly score: number;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly snippet: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SearchRerankRequest = {
|
|
17
|
+
readonly query: string;
|
|
18
|
+
/** Candidates in baseline order with unique ids. */
|
|
19
|
+
readonly candidates: readonly SearchRerankCandidate[];
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SearchRerankDetails = {
|
|
24
|
+
/** Observed model identity, not an unverified requested alias. */
|
|
25
|
+
readonly model?: string;
|
|
26
|
+
/** Known provider-reported totals; missing or incomplete usage is never zero-cost evidence. */
|
|
27
|
+
readonly usage?: {
|
|
28
|
+
readonly inputTokens?: number;
|
|
29
|
+
readonly outputTokens?: number;
|
|
30
|
+
};
|
|
31
|
+
readonly accounting?: {
|
|
32
|
+
readonly candidates: number;
|
|
33
|
+
/** Number of transport invocations, including calls with unknown outcomes. */
|
|
34
|
+
readonly attempted: number;
|
|
35
|
+
/** Transport invocations settled before this immutable result snapshot. */
|
|
36
|
+
readonly completed: number;
|
|
37
|
+
readonly elapsedMs: number;
|
|
38
|
+
readonly usageComplete: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type SearchRerankResult = SearchRerankDetails & (
|
|
43
|
+
| {
|
|
44
|
+
readonly status: "ready";
|
|
45
|
+
/** A permutation of the window ids the engine ranked. */
|
|
46
|
+
readonly ordering: readonly string[];
|
|
47
|
+
/** Engine-assigned relevance probability per candidate id. */
|
|
48
|
+
readonly probabilities: Readonly<Record<string, number>>;
|
|
49
|
+
readonly confidence?: number;
|
|
50
|
+
}
|
|
51
|
+
| { readonly status: "unavailable" | "failed"; readonly message: string });
|
|
52
|
+
|
|
53
|
+
export type SearchReranker = {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly rerank: (request: SearchRerankRequest) => Promise<SearchRerankResult>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type KnowledgeBaseSearchRerankOptions = {
|
|
59
|
+
readonly engine: "typesafe";
|
|
60
|
+
/** Window size submitted to the engine, from 2 through MAX_RERANK_CANDIDATES. */
|
|
61
|
+
readonly limit?: number;
|
|
62
|
+
/** Cancels only the hosted rerank phase, not local retrieval or indexing. */
|
|
63
|
+
readonly signal?: AbortSignal;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type RerankHitPlacement = {
|
|
67
|
+
readonly baselineRank: number;
|
|
68
|
+
readonly rerankRank: number;
|
|
69
|
+
readonly probability?: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type AppliedRerank<T extends { readonly id: string; readonly rank: number }> = {
|
|
73
|
+
readonly status: "ready" | "unavailable" | "failed";
|
|
74
|
+
/** Reordered window followed by non-window hits in baseline order. */
|
|
75
|
+
readonly hits: readonly T[];
|
|
76
|
+
/** Window hit placements keyed by id; empty when the result was not applied. */
|
|
77
|
+
readonly placements: ReadonlyMap<string, RerankHitPlacement>;
|
|
78
|
+
/** Strictly inspected engine result, or a generic failed result when malformed. */
|
|
79
|
+
readonly result: SearchRerankResult;
|
|
80
|
+
readonly message?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const MALFORMED_RERANK_RESULT_MESSAGE = "Rerank engine returned a malformed result.";
|
|
84
|
+
const MAX_RERANK_DIAGNOSTIC_BYTES = 512;
|
|
85
|
+
const MAX_RERANK_MODEL_BYTES = 128;
|
|
86
|
+
const MAX_RERANK_TOKEN_COUNT = 1_000_000_000;
|
|
87
|
+
|
|
88
|
+
function dataRecord(value: unknown): Record<string, unknown> | null {
|
|
89
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
90
|
+
const prototype = Object.getPrototypeOf(value);
|
|
91
|
+
if (prototype !== Object.prototype && prototype !== null) return null;
|
|
92
|
+
const keys = Reflect.ownKeys(value);
|
|
93
|
+
if (keys.length > MAX_RERANK_CANDIDATES) return null;
|
|
94
|
+
const output = Object.create(null) as Record<string, unknown>;
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
if (typeof key !== "string") return null;
|
|
97
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
98
|
+
if (
|
|
99
|
+
descriptor === undefined
|
|
100
|
+
|| !("value" in descriptor)
|
|
101
|
+
|| !descriptor.enumerable
|
|
102
|
+
) return null;
|
|
103
|
+
output[key] = descriptor.value;
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
109
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function boundedDiagnostic(value: unknown): value is string {
|
|
113
|
+
return typeof value === "string"
|
|
114
|
+
&& value !== ""
|
|
115
|
+
&& Buffer.byteLength(value, "utf8") <= MAX_RERANK_DIAGNOSTIC_BYTES
|
|
116
|
+
&& !/[\u0000-\u001f\u007f]/u.test(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tokenCount(value: unknown): value is number {
|
|
120
|
+
return Number.isSafeInteger(value)
|
|
121
|
+
&& (value as number) >= 0
|
|
122
|
+
&& (value as number) <= MAX_RERANK_TOKEN_COUNT;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inspectRerankDetails(result: Record<string, unknown>): SearchRerankDetails | null {
|
|
126
|
+
const model = result["model"];
|
|
127
|
+
if (
|
|
128
|
+
model !== undefined
|
|
129
|
+
&& (
|
|
130
|
+
typeof model !== "string"
|
|
131
|
+
|| !/^[\x20-\x7e]+$/u.test(model)
|
|
132
|
+
|| Buffer.byteLength(model, "utf8") > MAX_RERANK_MODEL_BYTES
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const rawUsage = result["usage"];
|
|
138
|
+
let usage: {
|
|
139
|
+
readonly inputTokens?: number;
|
|
140
|
+
readonly outputTokens?: number;
|
|
141
|
+
} | undefined;
|
|
142
|
+
if (rawUsage !== undefined) {
|
|
143
|
+
const usageRecord = dataRecord(rawUsage);
|
|
144
|
+
if (
|
|
145
|
+
usageRecord === null
|
|
146
|
+
|| !hasOnlyKeys(usageRecord, ["inputTokens", "outputTokens"])
|
|
147
|
+
|| (usageRecord["inputTokens"] !== undefined && !tokenCount(usageRecord["inputTokens"]))
|
|
148
|
+
|| (usageRecord["outputTokens"] !== undefined && !tokenCount(usageRecord["outputTokens"]))
|
|
149
|
+
) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
usage = {
|
|
153
|
+
...(usageRecord["inputTokens"] === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { inputTokens: usageRecord["inputTokens"] as number }),
|
|
156
|
+
...(usageRecord["outputTokens"] === undefined
|
|
157
|
+
? {}
|
|
158
|
+
: { outputTokens: usageRecord["outputTokens"] as number }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const rawAccounting = result["accounting"];
|
|
162
|
+
let accounting: SearchRerankDetails["accounting"];
|
|
163
|
+
if (rawAccounting !== undefined) {
|
|
164
|
+
const record = dataRecord(rawAccounting);
|
|
165
|
+
if (record === null || !hasOnlyKeys(record, ["candidates", "attempted", "completed", "elapsedMs", "usageComplete"])) return null;
|
|
166
|
+
const { candidates, attempted, completed, elapsedMs, usageComplete } = record;
|
|
167
|
+
if (
|
|
168
|
+
!Number.isSafeInteger(candidates) || (candidates as number) < 0 || (candidates as number) > MAX_RERANK_CANDIDATES
|
|
169
|
+
|| !Number.isSafeInteger(attempted) || (attempted as number) < 0 || (attempted as number) > (candidates as number)
|
|
170
|
+
|| !Number.isSafeInteger(completed) || (completed as number) < 0 || (completed as number) > (attempted as number)
|
|
171
|
+
|| typeof elapsedMs !== "number" || !Number.isFinite(elapsedMs) || elapsedMs < 0 || elapsedMs > 86_400_000
|
|
172
|
+
|| typeof usageComplete !== "boolean"
|
|
173
|
+
|| (usageComplete && (completed !== attempted || ((attempted as number) > 0 && (usage?.inputTokens === undefined || usage.outputTokens === undefined))))
|
|
174
|
+
) return null;
|
|
175
|
+
accounting = Object.freeze({ candidates: candidates as number, attempted: attempted as number,
|
|
176
|
+
completed: completed as number, elapsedMs, usageComplete });
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
...(model === undefined ? {} : { model }),
|
|
180
|
+
...(usage === undefined ? {} : { usage: Object.freeze(usage) }),
|
|
181
|
+
...(accounting === undefined ? {} : { accounting }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inspectRerankResult(value: unknown): SearchRerankResult {
|
|
186
|
+
let details: SearchRerankDetails = {};
|
|
187
|
+
const malformed = (): SearchRerankResult => ({
|
|
188
|
+
status: "failed", message: MALFORMED_RERANK_RESULT_MESSAGE, ...details,
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
const result = dataRecord(value);
|
|
192
|
+
if (result === null || typeof result["status"] !== "string") {
|
|
193
|
+
return malformed();
|
|
194
|
+
}
|
|
195
|
+
const inspectedDetails = inspectRerankDetails(result);
|
|
196
|
+
if (inspectedDetails === null) return malformed();
|
|
197
|
+
details = inspectedDetails;
|
|
198
|
+
if (result["status"] === "failed" || result["status"] === "unavailable") {
|
|
199
|
+
if (
|
|
200
|
+
!hasOnlyKeys(result, ["status", "message", "model", "usage", "accounting"])
|
|
201
|
+
|| !boundedDiagnostic(result["message"])
|
|
202
|
+
) {
|
|
203
|
+
return malformed();
|
|
204
|
+
}
|
|
205
|
+
return { status: result["status"], message: result["message"], ...details };
|
|
206
|
+
}
|
|
207
|
+
if (result["status"] !== "ready" || !hasOnlyKeys(
|
|
208
|
+
result,
|
|
209
|
+
["status", "ordering", "probabilities", "confidence", "model", "usage", "accounting"],
|
|
210
|
+
)) {
|
|
211
|
+
return malformed();
|
|
212
|
+
}
|
|
213
|
+
if (!Array.isArray(result["ordering"]) || result["ordering"].length > MAX_RERANK_CANDIDATES) {
|
|
214
|
+
return malformed();
|
|
215
|
+
}
|
|
216
|
+
const ordering: string[] = [];
|
|
217
|
+
const rawOrdering = result["ordering"] as readonly unknown[];
|
|
218
|
+
for (let index = 0; index < rawOrdering.length; index += 1) {
|
|
219
|
+
const descriptor = Object.getOwnPropertyDescriptor(rawOrdering, String(index));
|
|
220
|
+
const id = descriptor !== undefined && "value" in descriptor ? descriptor.value as unknown : undefined;
|
|
221
|
+
if (typeof id !== "string" || id === "" || Buffer.byteLength(id, "utf8") > MAX_RERANK_STATE_BYTES) {
|
|
222
|
+
return malformed();
|
|
223
|
+
}
|
|
224
|
+
ordering.push(id);
|
|
225
|
+
}
|
|
226
|
+
const rawProbabilities = dataRecord(result["probabilities"]);
|
|
227
|
+
if (rawProbabilities === null) {
|
|
228
|
+
return malformed();
|
|
229
|
+
}
|
|
230
|
+
const probabilities: Record<string, number> = Object.create(null) as Record<string, number>;
|
|
231
|
+
for (const [id, probability] of Object.entries(rawProbabilities)) {
|
|
232
|
+
if (
|
|
233
|
+
typeof probability !== "number"
|
|
234
|
+
|| !Number.isFinite(probability)
|
|
235
|
+
|| probability < 0
|
|
236
|
+
|| probability > 1
|
|
237
|
+
) {
|
|
238
|
+
return malformed();
|
|
239
|
+
}
|
|
240
|
+
probabilities[id] = probability;
|
|
241
|
+
}
|
|
242
|
+
const confidence = result["confidence"];
|
|
243
|
+
if (
|
|
244
|
+
confidence !== undefined
|
|
245
|
+
&& (
|
|
246
|
+
typeof confidence !== "number"
|
|
247
|
+
|| !Number.isFinite(confidence)
|
|
248
|
+
|| confidence < 0
|
|
249
|
+
|| confidence > 1
|
|
250
|
+
)
|
|
251
|
+
) {
|
|
252
|
+
return malformed();
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
status: "ready",
|
|
256
|
+
ordering: Object.freeze(ordering),
|
|
257
|
+
probabilities,
|
|
258
|
+
...(confidence === undefined ? {} : { confidence }),
|
|
259
|
+
...details,
|
|
260
|
+
};
|
|
261
|
+
} catch {
|
|
262
|
+
return malformed();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function failedRerank<T extends { readonly id: string; readonly rank: number }>(
|
|
267
|
+
hits: readonly T[],
|
|
268
|
+
message: string,
|
|
269
|
+
details: SearchRerankDetails,
|
|
270
|
+
): AppliedRerank<T> {
|
|
271
|
+
const result = { status: "failed" as const, message,
|
|
272
|
+
...(details.model === undefined ? {} : { model: details.model }),
|
|
273
|
+
...(details.usage === undefined ? {} : { usage: details.usage }),
|
|
274
|
+
...(details.accounting === undefined ? {} : { accounting: details.accounting }),
|
|
275
|
+
};
|
|
276
|
+
return { status: "failed", hits, placements: new Map(), result, message };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Apply one rerank result to hits in baseline order. The window is the leading
|
|
281
|
+
* `result.ordering.length` hits; exact identities stay first, then members are
|
|
282
|
+
* sorted by probability descending, baseline rank, and id. Later hits keep
|
|
283
|
+
* their baseline order and rank. A malformed result keeps the baseline order and reports
|
|
284
|
+
* `failed` so callers can surface a degraded diagnostic. When expected window
|
|
285
|
+
* ids are supplied, a partial candidate result is malformed rather than applied.
|
|
286
|
+
*/
|
|
287
|
+
export function applyRerank<T extends {
|
|
288
|
+
readonly id: string;
|
|
289
|
+
readonly rank: number;
|
|
290
|
+
readonly identity?: boolean;
|
|
291
|
+
}>(
|
|
292
|
+
hits: readonly T[],
|
|
293
|
+
result: unknown,
|
|
294
|
+
expectedWindowIds?: readonly string[],
|
|
295
|
+
): AppliedRerank<T> {
|
|
296
|
+
const inspected = inspectRerankResult(result);
|
|
297
|
+
if (inspected.status !== "ready") {
|
|
298
|
+
return {
|
|
299
|
+
status: inspected.status,
|
|
300
|
+
hits,
|
|
301
|
+
placements: new Map(),
|
|
302
|
+
result: inspected,
|
|
303
|
+
message: inspected.message,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const ordering = inspected.ordering;
|
|
307
|
+
if (
|
|
308
|
+
expectedWindowIds !== undefined
|
|
309
|
+
&& (
|
|
310
|
+
ordering.length !== expectedWindowIds.length
|
|
311
|
+
|| expectedWindowIds.some((id) => !ordering.includes(id))
|
|
312
|
+
)
|
|
313
|
+
) {
|
|
314
|
+
return failedRerank(hits, MALFORMED_RERANK_RESULT_MESSAGE, inspected);
|
|
315
|
+
}
|
|
316
|
+
const windowIds = new Set<string>();
|
|
317
|
+
for (const id of ordering) {
|
|
318
|
+
if (windowIds.has(id)) {
|
|
319
|
+
return failedRerank(
|
|
320
|
+
hits,
|
|
321
|
+
"Rerank ordering contained a duplicate candidate id.",
|
|
322
|
+
inspected,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
windowIds.add(id);
|
|
326
|
+
}
|
|
327
|
+
const window = hits.slice(0, ordering.length);
|
|
328
|
+
const windowById = new Map(window.map((hit) => [hit.id, hit]));
|
|
329
|
+
for (const id of ordering) {
|
|
330
|
+
if (!windowById.has(id)) {
|
|
331
|
+
return failedRerank(
|
|
332
|
+
hits,
|
|
333
|
+
"Rerank ordering contained an unknown candidate id.",
|
|
334
|
+
inspected,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const hit of window) {
|
|
339
|
+
if (!windowIds.has(hit.id)) {
|
|
340
|
+
return failedRerank(
|
|
341
|
+
hits,
|
|
342
|
+
"Rerank ordering omitted a window candidate id.",
|
|
343
|
+
inspected,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const probabilityKeys = Object.keys(inspected.probabilities);
|
|
348
|
+
for (const id of probabilityKeys) {
|
|
349
|
+
if (!windowIds.has(id)) {
|
|
350
|
+
return failedRerank(
|
|
351
|
+
hits,
|
|
352
|
+
"Rerank probabilities contained an unknown candidate id.",
|
|
353
|
+
inspected,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const id of ordering) {
|
|
358
|
+
if (inspected.probabilities[id] === undefined) {
|
|
359
|
+
return failedRerank(
|
|
360
|
+
hits,
|
|
361
|
+
"Rerank probability for a candidate id was missing or invalid.",
|
|
362
|
+
inspected,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const probabilityOf = (id: string): number => inspected.probabilities[id] as number;
|
|
367
|
+
const sorted = window.toSorted(
|
|
368
|
+
(left, right) =>
|
|
369
|
+
Number(right.identity === true) - Number(left.identity === true)
|
|
370
|
+
|| probabilityOf(right.id) - probabilityOf(left.id)
|
|
371
|
+
|| left.rank - right.rank
|
|
372
|
+
|| left.id.localeCompare(right.id),
|
|
373
|
+
);
|
|
374
|
+
const placements = new Map<string, RerankHitPlacement>();
|
|
375
|
+
const reordered = sorted.map((hit, index) => {
|
|
376
|
+
placements.set(hit.id, {
|
|
377
|
+
baselineRank: hit.rank,
|
|
378
|
+
rerankRank: index + 1,
|
|
379
|
+
probability: probabilityOf(hit.id),
|
|
380
|
+
});
|
|
381
|
+
return hit.rank === index + 1 ? hit : { ...hit, rank: index + 1 };
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
status: "ready",
|
|
385
|
+
hits: Object.freeze([...reordered, ...hits.slice(ordering.length)]),
|
|
386
|
+
placements,
|
|
387
|
+
result: inspected,
|
|
388
|
+
};
|
|
389
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -52,6 +52,15 @@ import {
|
|
|
52
52
|
type SearchRuleRequest,
|
|
53
53
|
type SearchRulesV1,
|
|
54
54
|
} from "./search-rules.js";
|
|
55
|
+
import {
|
|
56
|
+
applyRerank,
|
|
57
|
+
MAX_RERANK_CANDIDATES,
|
|
58
|
+
MAX_RERANK_SNIPPET_BYTES,
|
|
59
|
+
type KnowledgeBaseSearchRerankOptions,
|
|
60
|
+
type SearchRerankCandidate,
|
|
61
|
+
type SearchReranker,
|
|
62
|
+
type SearchRerankDetails,
|
|
63
|
+
} from "./rerank.js";
|
|
55
64
|
import {
|
|
56
65
|
openSemanticSearchSession,
|
|
57
66
|
recommendedEmbeddingModel,
|
|
@@ -120,6 +129,8 @@ export type KnowledgeBaseSearchOptions = {
|
|
|
120
129
|
readonly graph?: false | KnowledgeBaseGraphOptions;
|
|
121
130
|
/** Git provenance is separate from the primary relevance rank. */
|
|
122
131
|
readonly history?: false | "auto" | "required" | KnowledgeBaseHistoryOptions;
|
|
132
|
+
/** Opt-in rerank engine over a bounded result window. */
|
|
133
|
+
readonly rerank?: KnowledgeBaseSearchRerankOptions;
|
|
123
134
|
};
|
|
124
135
|
|
|
125
136
|
export type KnowledgeBaseExactEvidence = {
|
|
@@ -142,9 +153,21 @@ export type KnowledgeBaseQmdEvidence = {
|
|
|
142
153
|
readonly signals?: SemanticSearchHit["signals"];
|
|
143
154
|
};
|
|
144
155
|
|
|
156
|
+
export type KnowledgeBaseRerankEvidence = {
|
|
157
|
+
readonly kind: "rerank";
|
|
158
|
+
readonly engine: string;
|
|
159
|
+
/** One-based position before reranking. */
|
|
160
|
+
readonly baselineRank: number;
|
|
161
|
+
/** One-based position after reranking. */
|
|
162
|
+
readonly rerankRank: number;
|
|
163
|
+
/** Engine-assigned relevance probability; not the fused RRF score. */
|
|
164
|
+
readonly probability?: number;
|
|
165
|
+
};
|
|
166
|
+
|
|
145
167
|
export type KnowledgeBaseSearchEvidence =
|
|
146
168
|
| KnowledgeBaseExactEvidence
|
|
147
|
-
| KnowledgeBaseQmdEvidence
|
|
169
|
+
| KnowledgeBaseQmdEvidence
|
|
170
|
+
| KnowledgeBaseRerankEvidence;
|
|
148
171
|
|
|
149
172
|
export type KnowledgeBaseSearchHit = {
|
|
150
173
|
readonly id: string;
|
|
@@ -163,7 +186,9 @@ export type KnowledgeBaseSearchHit = {
|
|
|
163
186
|
};
|
|
164
187
|
|
|
165
188
|
export type KnowledgeBaseSearchDiagnostic = {
|
|
166
|
-
readonly lane: "exact" | "git" | "graph" | "qmd";
|
|
189
|
+
readonly lane: "exact" | "git" | "graph" | "qmd" | "rerank";
|
|
190
|
+
/** Structured hosted-lane accounting, including known usage after fallback. */
|
|
191
|
+
readonly rerank?: SearchRerankDetails;
|
|
167
192
|
readonly status: "degraded" | "ready" | "unavailable";
|
|
168
193
|
readonly results: number;
|
|
169
194
|
readonly message?: string;
|
|
@@ -244,6 +269,8 @@ export type KnowledgeBaseDependencies = {
|
|
|
244
269
|
readonly semanticSession?: SemanticSearchSession;
|
|
245
270
|
readonly git?: GitHistoryDependencies;
|
|
246
271
|
readonly indexGitHistory?: typeof indexGitHistory;
|
|
272
|
+
/** Opt-in rerank engines resolved by search option id. */
|
|
273
|
+
readonly rerankers?: readonly SearchReranker[];
|
|
247
274
|
};
|
|
248
275
|
|
|
249
276
|
export type KnowledgeBaseSession = {
|
|
@@ -302,6 +329,11 @@ function errorMessage(error: unknown): string {
|
|
|
302
329
|
return error instanceof Error ? error.message : String(error);
|
|
303
330
|
}
|
|
304
331
|
|
|
332
|
+
/** One-based rank to display for one evidence lane; rerank shows its final position. */
|
|
333
|
+
export function searchEvidenceRank(item: KnowledgeBaseSearchEvidence): number {
|
|
334
|
+
return item.kind === "rerank" ? item.rerankRank : item.rank;
|
|
335
|
+
}
|
|
336
|
+
|
|
305
337
|
function utf8Prefix(value: string, maximumBytes: number): {
|
|
306
338
|
readonly value: string;
|
|
307
339
|
readonly truncated: boolean;
|
|
@@ -489,6 +521,45 @@ export function validateKnowledgeBaseSearchHistory(value: unknown): void {
|
|
|
489
521
|
checkedHistoryRequest(value);
|
|
490
522
|
}
|
|
491
523
|
|
|
524
|
+
type CheckedRerankRequest =
|
|
525
|
+
| { readonly enabled: false }
|
|
526
|
+
| {
|
|
527
|
+
readonly enabled: true;
|
|
528
|
+
readonly limit: number;
|
|
529
|
+
readonly reranker: SearchReranker;
|
|
530
|
+
readonly signal?: AbortSignal;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
function checkedRerankRequest(
|
|
534
|
+
value: unknown,
|
|
535
|
+
rerankers: readonly SearchReranker[] | undefined,
|
|
536
|
+
): CheckedRerankRequest {
|
|
537
|
+
if (value === undefined) return { enabled: false };
|
|
538
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
539
|
+
throw new TypeError("Search rerank options must be an options object.");
|
|
540
|
+
}
|
|
541
|
+
const options = value as KnowledgeBaseSearchRerankOptions;
|
|
542
|
+
if (options.engine !== "typesafe") {
|
|
543
|
+
throw new Error('Search rerank engine must be "typesafe".');
|
|
544
|
+
}
|
|
545
|
+
const reranker = rerankers?.find((entry) => entry.id === options.engine);
|
|
546
|
+
if (reranker === undefined) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Search rerank engine ${JSON.stringify(options.engine)} is not available in this session.`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const limit = options.limit ?? MAX_RERANK_CANDIDATES;
|
|
552
|
+
if (!Number.isSafeInteger(limit) || limit < 2 || limit > MAX_RERANK_CANDIDATES) {
|
|
553
|
+
throw new RangeError(
|
|
554
|
+
`Search rerank limit must be an integer from 2 through ${MAX_RERANK_CANDIDATES}.`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
558
|
+
throw new TypeError("Search rerank signal must be an AbortSignal.");
|
|
559
|
+
}
|
|
560
|
+
return { enabled: true, limit, reranker, ...(options.signal === undefined ? {} : { signal: options.signal }) };
|
|
561
|
+
}
|
|
562
|
+
|
|
492
563
|
/** Open a read-only session that shares one live Markdown scan across retrieval tools. */
|
|
493
564
|
export async function openKnowledgeBase(
|
|
494
565
|
options: OpenKnowledgeBaseOptions,
|
|
@@ -696,6 +767,10 @@ export async function openKnowledgeBase(
|
|
|
696
767
|
}
|
|
697
768
|
const historyRequest = checkedHistoryRequest(effectiveOptions.history);
|
|
698
769
|
const graphOptions = checkedGraphOptions(effectiveOptions.graph);
|
|
770
|
+
const rerankRequest = checkedRerankRequest(
|
|
771
|
+
effectiveOptions.rerank,
|
|
772
|
+
dependencies.rerankers,
|
|
773
|
+
);
|
|
699
774
|
const explicitSeeds = graphOptions === null
|
|
700
775
|
? []
|
|
701
776
|
: resolvedGraphSeeds(snapshot.notes, graphOptions.related);
|
|
@@ -816,7 +891,10 @@ export async function openKnowledgeBase(
|
|
|
816
891
|
const shouldApplyPriority = ordering === "priority-then-relevance"
|
|
817
892
|
&& searchRules !== null
|
|
818
893
|
&& searchRules.priorityRules.length > 0;
|
|
819
|
-
const
|
|
894
|
+
const resultWindow = rerankRequest.enabled
|
|
895
|
+
? Math.max(limit, rerankRequest.limit)
|
|
896
|
+
: limit;
|
|
897
|
+
const relevanceResults = (shouldApplyPriority ? ordered : ordered.slice(0, resultWindow))
|
|
820
898
|
.map((candidate, index): KnowledgeBaseSearchHit => {
|
|
821
899
|
const note = notesById.get(candidate.id);
|
|
822
900
|
if (note === undefined) {
|
|
@@ -864,10 +942,83 @@ export async function openKnowledgeBase(
|
|
|
864
942
|
contributions: candidate.contributions,
|
|
865
943
|
};
|
|
866
944
|
});
|
|
945
|
+
let rankedResults: readonly KnowledgeBaseSearchHit[] = relevanceResults;
|
|
946
|
+
if (rerankRequest.enabled) {
|
|
947
|
+
const candidates: SearchRerankCandidate[] = relevanceResults
|
|
948
|
+
.slice(0, rerankRequest.limit)
|
|
949
|
+
.map((hit, index) => ({
|
|
950
|
+
id: hit.id,
|
|
951
|
+
baselineRank: index + 1,
|
|
952
|
+
score: hit.score,
|
|
953
|
+
title: hit.title,
|
|
954
|
+
path: hit.path,
|
|
955
|
+
snippet: utf8Prefix(hit.snippet, MAX_RERANK_SNIPPET_BYTES).value,
|
|
956
|
+
}));
|
|
957
|
+
const outcome = await Promise.resolve()
|
|
958
|
+
.then(() => rerankRequest.reranker.rerank({ query: effectiveQuery, candidates,
|
|
959
|
+
...(rerankRequest.signal === undefined ? {} : { signal: rerankRequest.signal }),
|
|
960
|
+
}))
|
|
961
|
+
.then(
|
|
962
|
+
(result) => ({ ok: true as const, result }),
|
|
963
|
+
(error: unknown) => ({ ok: false as const, error }),
|
|
964
|
+
);
|
|
965
|
+
const applied = applyRerank(
|
|
966
|
+
relevanceResults,
|
|
967
|
+
outcome.ok
|
|
968
|
+
? outcome.result
|
|
969
|
+
: {
|
|
970
|
+
status: "failed",
|
|
971
|
+
message: "Rerank engine failed before returning a result.",
|
|
972
|
+
},
|
|
973
|
+
candidates.map(({ id }) => id),
|
|
974
|
+
);
|
|
975
|
+
rankedResults = applied.hits.map((hit) => {
|
|
976
|
+
const placement = applied.placements.get(hit.id);
|
|
977
|
+
if (placement === undefined) return hit;
|
|
978
|
+
return {
|
|
979
|
+
...hit,
|
|
980
|
+
evidence: [
|
|
981
|
+
...hit.evidence,
|
|
982
|
+
{
|
|
983
|
+
kind: "rerank" as const,
|
|
984
|
+
engine: rerankRequest.reranker.id,
|
|
985
|
+
baselineRank: placement.baselineRank,
|
|
986
|
+
rerankRank: placement.rerankRank,
|
|
987
|
+
...(placement.probability === undefined
|
|
988
|
+
? {}
|
|
989
|
+
: { probability: placement.probability }),
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
};
|
|
993
|
+
});
|
|
994
|
+
const resultDetails: SearchRerankDetails = {
|
|
995
|
+
...(applied.result.model === undefined ? {} : { model: applied.result.model }),
|
|
996
|
+
...(applied.result.usage === undefined ? {} : { usage: applied.result.usage }),
|
|
997
|
+
...(applied.result.accounting === undefined ? {} : { accounting: applied.result.accounting }),
|
|
998
|
+
};
|
|
999
|
+
const details: string[] = [];
|
|
1000
|
+
if (resultDetails.model !== undefined) details.push(resultDetails.model);
|
|
1001
|
+
if (resultDetails.usage !== undefined) {
|
|
1002
|
+
details.push(
|
|
1003
|
+
`${(resultDetails.usage.inputTokens ?? 0).toLocaleString("en-US")} input tokens, `
|
|
1004
|
+
+ `${(resultDetails.usage.outputTokens ?? 0).toLocaleString("en-US")} output tokens`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
const rerankMessage = applied.status === "ready"
|
|
1008
|
+
? (details.length === 0 ? undefined : details.join("; "))
|
|
1009
|
+
: (applied.message ?? "Rerank engine did not return a result.");
|
|
1010
|
+
diagnostics.push({
|
|
1011
|
+
lane: "rerank",
|
|
1012
|
+
...(Object.keys(resultDetails).length === 0 ? {} : { rerank: resultDetails }),
|
|
1013
|
+
status: applied.status === "failed" ? "degraded" : applied.status,
|
|
1014
|
+
results: applied.status === "ready" ? applied.placements.size : 0,
|
|
1015
|
+
...(rerankMessage === undefined ? {} : { message: rerankMessage }),
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
867
1018
|
let priorityTrace: readonly SearchPriorityTrace[] | null = null;
|
|
868
|
-
let results: readonly KnowledgeBaseSearchHit[] =
|
|
1019
|
+
let results: readonly KnowledgeBaseSearchHit[] = rankedResults.slice(0, limit);
|
|
869
1020
|
if (shouldApplyPriority && searchRules !== null) {
|
|
870
|
-
const prioritized = prioritizeSearchHits(
|
|
1021
|
+
const prioritized = prioritizeSearchHits(rankedResults, searchRules, {
|
|
871
1022
|
...(options.vaultId === undefined ? {} : { vaultId: options.vaultId }),
|
|
872
1023
|
});
|
|
873
1024
|
const selectedHits = prioritized.hits.slice(0, limit);
|
|
@@ -1119,7 +1270,7 @@ export function packSearchContext(
|
|
|
1119
1270
|
if (index > 0 && !append(", ")) return writer.result();
|
|
1120
1271
|
if (!append(item.kind)) return writer.result();
|
|
1121
1272
|
if (!append("#")) return writer.result();
|
|
1122
|
-
if (!append(String(item
|
|
1273
|
+
if (!append(String(searchEvidenceRank(item)))) return writer.result();
|
|
1123
1274
|
}
|
|
1124
1275
|
if (!append("\n\n")) return writer.result();
|
|
1125
1276
|
if (!append(hit.snippet)) return writer.result();
|