@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.
Files changed (93) hide show
  1. package/README.md +170 -64
  2. package/dist/authoring.js +5 -3
  3. package/dist/{authority-4sp9d552.js → authority-06eqhqam.js} +57 -12
  4. package/dist/benchmark.js +4 -4
  5. package/dist/capture.js +6 -5
  6. package/dist/cli.js +39 -33
  7. package/dist/clip/acquire.js +3 -2
  8. package/dist/clip/cli.js +6 -5
  9. package/dist/clip/doctor.js +4 -3
  10. package/dist/clip/jobs.js +3 -3
  11. package/dist/clip/refresh.js +1 -1
  12. package/dist/evaluation-builder.js +20 -19
  13. package/dist/evaluation-kb.js +16 -15
  14. package/dist/graph-authority.js +5 -5
  15. package/dist/graph-percolation.js +7 -7
  16. package/dist/graph.js +1 -1
  17. package/dist/{index-pbr6rv47.js → index-1k595z6v.js} +6 -6
  18. package/dist/{index-0k2x4nn9.js → index-233z9wmn.js} +17 -5
  19. package/dist/index-3agn8scn.js +306 -0
  20. package/dist/index-4knsp9qj.js +34 -0
  21. package/dist/index-66pshdtx.js +341 -0
  22. package/dist/{index-59ssmqqm.js → index-76dbn6pe.js} +1 -1
  23. package/dist/{index-0rfcqab6.js → index-810nfzdj.js} +617 -66
  24. package/dist/index-9rf81m0p.js +368 -0
  25. package/dist/{index-51bap0mq.js → index-bqtqeak3.js} +10 -10
  26. package/dist/{index-9zcpdbjg.js → index-de2w8crk.js} +13 -13
  27. package/dist/{index-bgfzwt4h.js → index-djrcc1yf.js} +6 -6
  28. package/dist/{index-6sw24nvv.js → index-ecqpgr5y.js} +53 -2
  29. package/dist/{index-van48n3g.js → index-fc4dr114.js} +4 -2
  30. package/dist/{index-jzcws0zn.js → index-g5vsqmdy.js} +27 -57
  31. package/dist/index-hmw17zaa.js +1985 -0
  32. package/dist/index-j70m75wd.js +217 -0
  33. package/dist/{index-nd6nynv2.js → index-jk1sgx36.js} +1 -1
  34. package/dist/{index-3ehtt59c.js → index-n386evxj.js} +1 -1
  35. package/dist/{index-vdw5yjfa.js → index-q7tbg07z.js} +110 -20
  36. package/dist/{index-ekpwvbra.js → index-qbssx940.js} +1 -2
  37. package/dist/{index-115b07ap.js → index-rr9kzq4n.js} +5 -5
  38. package/dist/{index-11621h23.js → index-tf4wzjew.js} +1 -1
  39. package/dist/{index-why54q5p.js → index-vnybgywh.js} +4 -4
  40. package/dist/{index-bcfn9xah.js → index-xv80vzfp.js} +7 -7
  41. package/dist/index.js +289 -95
  42. package/dist/percolate.js +2 -2
  43. package/dist/portfolio.js +16 -15
  44. package/dist/publish-model.js +36 -0
  45. package/dist/publish-reader/reader.css +1042 -0
  46. package/dist/publish-reader/reader.js +1534 -0
  47. package/dist/publish-reader/theme.js +92 -0
  48. package/dist/publish-search.js +52 -0
  49. package/dist/publish.js +36 -0
  50. package/dist/rerank-typesafe.js +13 -0
  51. package/dist/rerank.js +14 -0
  52. package/dist/sdk.js +16 -13
  53. package/dist/search.js +3 -3
  54. package/dist/semantic.js +5 -5
  55. package/dist/{support-vm4mkw0k.js → support-1rh2d7mt.js} +38 -32
  56. package/dist/workflows/decision-context.js +15 -14
  57. package/dist/workflows/index.js +15 -14
  58. package/package.json +65 -6
  59. package/skills/wordcell/AGENTS.md +1 -0
  60. package/skills/wordcell/SKILL.md +2 -1
  61. package/skills/wordcell/agents/openai.yaml +1 -1
  62. package/skills/wordcell/references/publish.md +127 -0
  63. package/skills/wordcell/references/query.md +22 -0
  64. package/skills/wordcell/references/refresh.md +14 -0
  65. package/src/authoring-model.ts +29 -0
  66. package/src/authoring-program.ts +38 -3
  67. package/src/authoring.ts +17 -3
  68. package/src/cli-program.ts +441 -12
  69. package/src/graph.ts +1 -1
  70. package/src/index.ts +4 -0
  71. package/src/oh/authority.ts +14 -5
  72. package/src/oh/canonical-rust.ts +127 -0
  73. package/src/oh/projection-rust.ts +63 -0
  74. package/src/oh-adoption.ts +7 -1
  75. package/src/publish-graph.ts +0 -0
  76. package/src/publish-index.ts +195 -0
  77. package/src/publish-markdown.ts +617 -0
  78. package/src/publish-model.ts +599 -0
  79. package/src/publish-nav.ts +204 -0
  80. package/src/publish-pages.ts +382 -0
  81. package/src/publish-reader/reader.css +1042 -0
  82. package/src/publish-reader/reader.ts +1017 -0
  83. package/src/publish-reader/theme.ts +97 -0
  84. package/src/publish-search.ts +418 -0
  85. package/src/publish-select.ts +419 -0
  86. package/src/publish-theme.ts +106 -0
  87. package/src/publish.ts +904 -0
  88. package/src/rerank-credentials.ts +56 -0
  89. package/src/rerank-typesafe.ts +472 -0
  90. package/src/rerank.ts +389 -0
  91. package/src/sdk.ts +157 -6
  92. package/src/serve.ts +180 -0
  93. 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 relevanceResults = (shouldApplyPriority ? ordered : ordered.slice(0, limit))
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[] = relevanceResults.slice(0, limit);
1019
+ let results: readonly KnowledgeBaseSearchHit[] = rankedResults.slice(0, limit);
869
1020
  if (shouldApplyPriority && searchRules !== null) {
870
- const prioritized = prioritizeSearchHits(relevanceResults, searchRules, {
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.rank))) return writer.result();
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();