@xdarkicex/openclaw-memory-libravdb 1.4.5 → 1.4.7

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 (75) hide show
  1. package/HOOK.md +14 -0
  2. package/README.md +32 -2
  3. package/dist/cli.d.ts +39 -0
  4. package/dist/cli.js +208 -0
  5. package/dist/context-engine.d.ts +56 -0
  6. package/dist/context-engine.js +125 -0
  7. package/dist/dream-promotion.d.ts +47 -0
  8. package/dist/dream-promotion.js +363 -0
  9. package/dist/dream-routing.d.ts +6 -0
  10. package/dist/dream-routing.js +31 -0
  11. package/dist/durable-namespace.d.ts +6 -0
  12. package/dist/durable-namespace.js +24 -0
  13. package/dist/grpc-client.d.ts +23 -0
  14. package/dist/grpc-client.js +104 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +40 -0
  17. package/dist/lifecycle-hooks.d.ts +4 -0
  18. package/dist/lifecycle-hooks.js +64 -0
  19. package/dist/markdown-hash.d.ts +3 -0
  20. package/dist/markdown-hash.js +82 -0
  21. package/dist/markdown-ingest.d.ts +43 -0
  22. package/dist/markdown-ingest.js +464 -0
  23. package/dist/memory-provider.d.ts +4 -0
  24. package/dist/memory-provider.js +13 -0
  25. package/dist/memory-runtime.d.ts +118 -0
  26. package/dist/memory-runtime.js +217 -0
  27. package/dist/plugin-runtime.d.ts +28 -0
  28. package/dist/plugin-runtime.js +127 -0
  29. package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
  30. package/dist/recall-cache.d.ts +2 -0
  31. package/dist/recall-cache.js +30 -0
  32. package/dist/rpc-protobuf-codecs.d.ts +70 -0
  33. package/dist/rpc-protobuf-codecs.js +77 -0
  34. package/dist/rpc.d.ts +14 -0
  35. package/dist/rpc.js +121 -0
  36. package/dist/sidecar.d.ts +34 -0
  37. package/dist/sidecar.js +535 -0
  38. package/dist/types.d.ts +163 -0
  39. package/dist/types.js +1 -0
  40. package/docs/contributing.md +14 -13
  41. package/docs/install.md +7 -9
  42. package/docs/installation.md +23 -16
  43. package/docs/uninstall.md +1 -1
  44. package/index.js +2 -0
  45. package/openclaw.plugin.json +2 -2
  46. package/package.json +39 -16
  47. package/packaging/README.md +0 -71
  48. package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
  49. package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
  50. package/packaging/systemd/libravdbd.service +0 -12
  51. package/src/cli.ts +0 -299
  52. package/src/comparison-experiments.ts +0 -128
  53. package/src/context-engine.ts +0 -1451
  54. package/src/continuity.ts +0 -93
  55. package/src/dream-promotion.ts +0 -492
  56. package/src/dream-routing.ts +0 -40
  57. package/src/durable-namespace.ts +0 -34
  58. package/src/index.ts +0 -47
  59. package/src/lifecycle-hooks.ts +0 -96
  60. package/src/markdown-hash.ts +0 -104
  61. package/src/markdown-ingest.ts +0 -627
  62. package/src/memory-provider.ts +0 -25
  63. package/src/memory-runtime.ts +0 -283
  64. package/src/openclaw-plugin-sdk.d.ts +0 -59
  65. package/src/plugin-runtime.ts +0 -116
  66. package/src/recall-cache.ts +0 -34
  67. package/src/recall-utils.ts +0 -131
  68. package/src/rpc.ts +0 -84
  69. package/src/scoring.ts +0 -632
  70. package/src/sidecar.ts +0 -486
  71. package/src/temporal.ts +0 -1010
  72. package/src/tokens.ts +0 -52
  73. package/src/types.ts +0 -277
  74. package/tsconfig.json +0 -20
  75. package/tsconfig.tests.json +0 -12
package/src/rpc.ts DELETED
@@ -1,84 +0,0 @@
1
- import type { RpcCallOptions, SidecarSocket } from "./types.js";
2
-
3
- interface PendingCall {
4
- resolve(value: unknown): void;
5
- reject(error: Error): void;
6
- timer: ReturnType<typeof setTimeout>;
7
- }
8
-
9
- export class RpcClient {
10
- private seq = 0;
11
- private readonly pending = new Map<number, PendingCall>();
12
- private buf = "";
13
-
14
- constructor(
15
- private readonly socket: SidecarSocket,
16
- private readonly options: RpcCallOptions,
17
- ) {
18
- socket.setEncoding("utf8");
19
- socket.on("data", (chunk) => this.handleData(chunk));
20
- socket.on("close", () => this.rejectAll(new Error("Socket closed")));
21
- }
22
-
23
- async call<T>(method: string, _params: unknown): Promise<T> {
24
- return await new Promise<T>((resolve, reject) => {
25
- const id = ++this.seq;
26
- const timer = setTimeout(() => {
27
- this.pending.delete(id);
28
- reject(new Error(`RPC timeout: ${method} (${this.options.timeoutMs}ms)`));
29
- }, this.options.timeoutMs);
30
-
31
- this.pending.set(id, { resolve, reject, timer });
32
- this.socket.write(
33
- `${JSON.stringify({ jsonrpc: "2.0", id, method, params: _params })}\n`,
34
- );
35
- });
36
- }
37
-
38
- private handleData(chunk: string): void {
39
- this.buf += chunk;
40
- const lines = this.buf.split("\n");
41
- this.buf = lines.pop() ?? "";
42
-
43
- for (const line of lines) {
44
- if (!line.trim()) {
45
- continue;
46
- }
47
-
48
- try {
49
- const msg = JSON.parse(line) as {
50
- id?: number;
51
- result?: unknown;
52
- error?: { message?: string };
53
- };
54
- if (typeof msg.id !== "number") {
55
- continue;
56
- }
57
-
58
- const pending = this.pending.get(msg.id);
59
- if (!pending) {
60
- continue;
61
- }
62
-
63
- clearTimeout(pending.timer);
64
- this.pending.delete(msg.id);
65
-
66
- if (msg.error?.message) {
67
- pending.reject(new Error(msg.error.message));
68
- } else {
69
- pending.resolve(msg.result);
70
- }
71
- } catch {
72
- // Ignore malformed frames and keep parsing future lines.
73
- }
74
- }
75
- }
76
-
77
- private rejectAll(error: Error): void {
78
- for (const [id, pending] of this.pending.entries()) {
79
- clearTimeout(pending.timer);
80
- this.pending.delete(id);
81
- pending.reject(error);
82
- }
83
- }
84
- }
package/src/scoring.ts DELETED
@@ -1,632 +0,0 @@
1
- import type { SearchResult } from "./types.js";
2
- import { getTemporalAnchorDensity } from "./temporal.js";
3
-
4
- interface HybridOptions {
5
- alpha?: number;
6
- beta?: number;
7
- gamma?: number;
8
- delta?: number;
9
- recencyLambdaSession?: number;
10
- recencyLambdaUser?: number;
11
- recencyLambdaGlobal?: number;
12
- sessionId: string;
13
- userId: string;
14
- }
15
-
16
- interface Section7Options {
17
- queryText: string;
18
- sessionId: string;
19
- userId: string;
20
- k1?: number;
21
- k2?: number;
22
- theta1?: number;
23
- kappa?: number;
24
- authorityRecencyLambda?: number;
25
- authorityRecencyWeight?: number;
26
- authorityFrequencyWeight?: number;
27
- authorityAuthoredWeight?: number;
28
- nowMs?: number;
29
- }
30
-
31
- interface HopOptions {
32
- etaHop?: number;
33
- thetaHop?: number;
34
- }
35
-
36
- interface RawUserRecoveryOptions {
37
- queryText: string;
38
- nowMs?: number;
39
- recencyLambda?: number;
40
- }
41
-
42
- export interface RawUserRecoveryDebugCandidate {
43
- id: string;
44
- text: string;
45
- temporalAnchorDensity: number;
46
- semanticScore: number;
47
- lexicalCoverage: number;
48
- recencyScore: number;
49
- finalScore: number;
50
- rationale: string;
51
- }
52
-
53
- interface ExpansionOptions {
54
- confidenceThreshold?: number;
55
- maxDepth?: number;
56
- tokenBudget?: number;
57
- penaltyFactor?: number;
58
- }
59
-
60
- export interface RecoveryTriggerResult {
61
- signal1CascadeTier3: boolean;
62
- signal2TopScoreBelowFloor: boolean;
63
- signal3AllSummariesLowConfidence: boolean;
64
- fire: boolean;
65
- }
66
-
67
- interface RetrievalFailureOptions {
68
- floorScore?: number;
69
- minTopK?: number;
70
- meanConfidenceThresh?: number;
71
- }
72
-
73
- export function detectRetrievalFailure(
74
- ranked: SearchResult[],
75
- opts: RetrievalFailureOptions = {},
76
- ): RecoveryTriggerResult {
77
- if (ranked.length === 0) {
78
- return {
79
- signal1CascadeTier3: false,
80
- signal2TopScoreBelowFloor: false,
81
- signal3AllSummariesLowConfidence: false,
82
- fire: false,
83
- };
84
- }
85
- const floorScore = opts.floorScore ?? 0.15;
86
- const minTopK = Math.max(1, Math.floor(opts.minTopK ?? 4));
87
- const meanConfidenceThresh = clamp01(opts.meanConfidenceThresh ?? 0.5);
88
-
89
- // Signal 1: cascade exhaustion (cascade_tier === 3 present)
90
- const signal1CascadeTier3 = ranked.some(
91
- (item) => item.metadata.cascade_tier === 3,
92
- );
93
-
94
- // Signal 2: top score below floor
95
- const topScore = ranked[0]!.finalScore ?? 0;
96
- const signal2TopScoreBelowFloor = topScore < floorScore;
97
-
98
- // Signal 3: top-k items are all summaries with low mean confidence
99
- const topK = ranked.slice(0, Math.min(minTopK, ranked.length));
100
- const allSummaries = topK.length > 0 && topK.every((item) => item.metadata.type === "summary");
101
- const meanConfidence =
102
- allSummaries && topK.length > 0
103
- ? topK.reduce(
104
- (sum, item) => sum + (typeof item.metadata.confidence === "number" ? item.metadata.confidence : 0),
105
- 0,
106
- ) / topK.length
107
- : NaN;
108
- const signal3AllSummariesLowConfidence =
109
- allSummaries && topK.length >= minTopK && meanConfidence < meanConfidenceThresh;
110
-
111
- // Composite: (S1 AND S2) OR S3
112
- const fire = (signal1CascadeTier3 && signal2TopScoreBelowFloor) || signal3AllSummariesLowConfidence;
113
-
114
- return {
115
- signal1CascadeTier3,
116
- signal2TopScoreBelowFloor,
117
- signal3AllSummariesLowConfidence,
118
- fire,
119
- };
120
- }
121
-
122
- export function expandSummaryCandidates(
123
- items: SearchResult[],
124
- expandFn: (sessionId: string, summaryId: string, maxDepth: number) => Promise<SearchResult[]>,
125
- sessionId: string,
126
- opts: ExpansionOptions,
127
- ): Promise<SearchResult[]> {
128
- const confidenceThreshold = opts.confidenceThreshold ?? 0.7;
129
- const maxDepth = opts.maxDepth ?? 2;
130
- const penaltyFactor = opts.penaltyFactor ?? 0.85;
131
- const tokenBudget = typeof opts.tokenBudget === "number" ? Math.max(0, opts.tokenBudget) : Number.POSITIVE_INFINITY;
132
-
133
- return (async () => {
134
- const out: SearchResult[] = [];
135
- let remainingBudget = tokenBudget;
136
-
137
- for (const summary of items) {
138
- const conf = typeof summary.metadata.confidence === "number" ? summary.metadata.confidence : 0;
139
- if (summary.metadata.type !== "summary" || conf < confidenceThreshold) {
140
- continue;
141
- }
142
- if (Number.isFinite(tokenBudget) && remainingBudget <= 0) {
143
- break;
144
- }
145
-
146
- const rawChildren = await expandFn(sessionId, summary.id, maxDepth);
147
- for (const child of rawChildren) {
148
- const cost = childTokenCost(child);
149
- if (!Number.isFinite(cost)) {
150
- continue;
151
- }
152
- if (Number.isFinite(tokenBudget) && cost > remainingBudget) {
153
- continue;
154
- }
155
- if (Number.isFinite(tokenBudget)) {
156
- remainingBudget -= cost;
157
- }
158
- out.push({
159
- ...child,
160
- metadata: {
161
- ...child.metadata,
162
- expanded_from_summary: true,
163
- parent_summary_id: summary.id,
164
- expansion_depth: (typeof summary.metadata.expansion_depth === "number" ? summary.metadata.expansion_depth : 0) + 1,
165
- },
166
- finalScore: clamp01((child.finalScore ?? child.score) * penaltyFactor),
167
- });
168
- }
169
- }
170
-
171
- return out;
172
- })();
173
- }
174
-
175
- export function mergeSection7VariantCandidates(
176
- ranked: SearchResult[],
177
- hopExpanded: SearchResult[],
178
- ): SearchResult[] {
179
- const byID = new Map<string, SearchResult>();
180
- for (const item of [...ranked, ...hopExpanded]) {
181
- const existing = byID.get(item.id);
182
- if (!existing || (item.finalScore ?? 0) > (existing.finalScore ?? 0)) {
183
- byID.set(item.id, item);
184
- }
185
- }
186
- return [...byID.values()].sort((left, right) => (right.finalScore ?? 0) - (left.finalScore ?? 0));
187
- }
188
-
189
- export function scoreCandidates(items: SearchResult[], opts: HybridOptions): SearchResult[] {
190
- const now = Date.now();
191
- const { alpha, beta, gamma } = normalizeWeights(
192
- opts.alpha ?? 0.7,
193
- opts.beta ?? 0.2,
194
- opts.gamma ?? 0.1,
195
- );
196
- const delta = clamp01(opts.delta ?? 0.5);
197
- // Lambda units are per-second decay constants.
198
- const recencyLambdaSession = Math.max(0, opts.recencyLambdaSession ?? 0.0001);
199
- const recencyLambdaUser = Math.max(0, opts.recencyLambdaUser ?? 0.00001);
200
- const recencyLambdaGlobal = Math.max(0, opts.recencyLambdaGlobal ?? 0.000002);
201
-
202
- return items
203
- .map((item) => {
204
- const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : now;
205
- const lambda =
206
- item.metadata.sessionId === opts.sessionId ? recencyLambdaSession
207
- : item.metadata.userId === opts.userId ? recencyLambdaUser
208
- : recencyLambdaGlobal;
209
- const ageSeconds = Math.max(0, now - ts) / 1000;
210
- const recency = Math.exp(-lambda * ageSeconds);
211
- const scopeBoost =
212
- item.metadata.sessionId === opts.sessionId ? 1.0
213
- : item.metadata.userId === opts.userId ? 0.6
214
- : 0.3;
215
- const similarity = clamp01(item.score);
216
- const baseScore =
217
- alpha * similarity +
218
- beta * recency +
219
- gamma * scopeBoost;
220
- const rawDecayRate =
221
- typeof item.metadata.decay_rate === "number" ? item.metadata.decay_rate : 0.0;
222
- const decayRate = Math.min(1, Math.max(0, rawDecayRate));
223
- const quality =
224
- item.metadata.type === "summary"
225
- ? 1.0 - delta * decayRate
226
- : 1.0;
227
- const finalScore = clamp01(baseScore * quality);
228
-
229
- return {
230
- ...item,
231
- finalScore,
232
- };
233
- })
234
- .sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
235
- }
236
-
237
- export function rankSection7VariantCandidates(items: SearchResult[], opts: Section7Options): SearchResult[] {
238
- const now = opts.nowMs ?? Date.now();
239
- const k1 = Math.max(1, Math.floor(opts.k1 ?? 16));
240
- const k2 = Math.max(1, Math.floor(opts.k2 ?? 8));
241
- const theta1 = clampSimilarity(opts.theta1 ?? 0.2);
242
- const kappa = Math.max(0, opts.kappa ?? 0.3);
243
- const { alpha: alphaR, beta: alphaF, gamma: alphaA } = normalizeWeights(
244
- opts.authorityRecencyWeight ?? 0.5,
245
- opts.authorityFrequencyWeight ?? 0.2,
246
- opts.authorityAuthoredWeight ?? 0.3,
247
- );
248
- const authorityRecencyLambda = Math.max(0, opts.authorityRecencyLambda ?? 0.00001);
249
-
250
- const deduped = dedupeCandidates(items);
251
- const coarseRaw = [...deduped]
252
- .sort((left, right) => similarity(right) - similarity(left))
253
- .slice(0, k1);
254
- const coarseFiltered = coarseRaw.filter((item) => similarity(item) >= theta1);
255
- const maxAccessCount = coarseFiltered.reduce((max, item) => Math.max(max, accessCount(item)), 0);
256
- const keywords = extractKeywords(opts.queryText);
257
-
258
- return coarseFiltered
259
- .map((item) => {
260
- const omega = authorityWeight(item, {
261
- now,
262
- authorityRecencyLambda,
263
- alphaR,
264
- alphaF,
265
- alphaA,
266
- maxAccessCount,
267
- });
268
- const sim = Math.max(similarity(item), 0);
269
- const keywordCoverage = normalizedKeywordCoverage(keywords, item.text);
270
- const finalScore = omega * sim * ((1 + kappa * keywordCoverage) / (1 + kappa));
271
-
272
- return {
273
- ...item,
274
- finalScore: clamp01(finalScore),
275
- };
276
- })
277
- .sort((left, right) => (right.finalScore ?? 0) - (left.finalScore ?? 0))
278
- .slice(0, Math.min(k2, coarseFiltered.length));
279
- }
280
-
281
- export function expandSection7HopCandidates(
282
- ranked: SearchResult[],
283
- authoredVariantRecords: SearchResult[],
284
- opts: HopOptions,
285
- ): SearchResult[] {
286
- const etaHop = clampOpenUnit(opts.etaHop ?? 0.5);
287
- const thetaHop = clamp01(opts.thetaHop ?? 0.15);
288
- const rankedIDs = new Set(ranked.map((item) => item.id));
289
- const authoredByID = new Map(authoredVariantRecords.map((item) => [item.id, item] as const));
290
- const bestScores = new Map<string, number>();
291
-
292
- for (const parent of ranked) {
293
- const parentScore = clamp01(parent.finalScore ?? 0);
294
- for (const targetID of hopTargets(parent)) {
295
- if (rankedIDs.has(targetID)) {
296
- continue;
297
- }
298
- if (!authoredByID.has(targetID)) {
299
- continue;
300
- }
301
- const candidateScore = etaHop * parentScore;
302
- if (candidateScore > (bestScores.get(targetID) ?? -1)) {
303
- bestScores.set(targetID, candidateScore);
304
- }
305
- }
306
- }
307
-
308
- return [...bestScores.entries()]
309
- .filter(([, score]) => score >= thetaHop)
310
- .map(([id, score]) => ({
311
- ...authoredByID.get(id)!,
312
- finalScore: score,
313
- }))
314
- .sort((left, right) => (right.finalScore ?? 0) - (left.finalScore ?? 0));
315
- }
316
-
317
- export function rankRawUserRecoveryCandidates(
318
- items: SearchResult[],
319
- opts: RawUserRecoveryOptions,
320
- ): { ranked: SearchResult[]; debug: RawUserRecoveryDebugCandidate[] } {
321
- const now = opts.nowMs ?? Date.now();
322
- const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
323
- const keywords = extractKeywords(opts.queryText);
324
- const intentPhrases = extractIntentPhrases(opts.queryText);
325
-
326
- const ranked = items
327
- .map((item) => {
328
- const semanticScore = clamp01(typeof item.score === "number" ? item.score : 0);
329
- const lexicalCoverage = normalizedKeywordCoverage(keywords, item.text);
330
- const recencyScore = computeRecencyScore(item, now, recencyLambda);
331
- const temporalAnchorDensity = getTemporalAnchorDensity(
332
- `${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
333
- item.text,
334
- );
335
- const intentAlignmentBonus = computeIntentAlignmentBonus(item.text, intentPhrases);
336
- const finalScore = clamp01(
337
- // Raw recovery is a precision-sensitive fallback: exact-turn recall is
338
- // usually better served by lexical overlap than by broad semantic drift.
339
- (0.20 * semanticScore) +
340
- (0.70 * lexicalCoverage) +
341
- (0.10 * recencyScore) +
342
- intentAlignmentBonus,
343
- );
344
- const rationale = buildRawUserRecoveryRationale({
345
- semanticScore,
346
- lexicalCoverage,
347
- recencyScore,
348
- intentAlignmentBonus,
349
- });
350
-
351
- return {
352
- ranked: {
353
- ...item,
354
- finalScore,
355
- },
356
- debug: {
357
- id: item.id,
358
- text: item.text,
359
- temporalAnchorDensity,
360
- semanticScore,
361
- lexicalCoverage,
362
- recencyScore,
363
- finalScore,
364
- rationale,
365
- },
366
- };
367
- })
368
- .sort((left, right) => {
369
- if (right.ranked.finalScore !== left.ranked.finalScore) {
370
- return (right.ranked.finalScore ?? 0) - (left.ranked.finalScore ?? 0);
371
- }
372
- if (right.debug.lexicalCoverage !== left.debug.lexicalCoverage) {
373
- return right.debug.lexicalCoverage - left.debug.lexicalCoverage;
374
- }
375
- if (right.debug.semanticScore !== left.debug.semanticScore) {
376
- return right.debug.semanticScore - left.debug.semanticScore;
377
- }
378
- return left.ranked.id.localeCompare(right.ranked.id);
379
- });
380
-
381
- return {
382
- ranked: ranked.map((entry) => entry.ranked),
383
- debug: ranked.map((entry) => entry.debug),
384
- };
385
- }
386
-
387
- function clamp01(value: number): number {
388
- return Math.min(1, Math.max(0, value));
389
- }
390
-
391
- function childTokenCost(item: SearchResult): number {
392
- const estimate = item.metadata.token_estimate;
393
- if (typeof estimate === "number" && Number.isFinite(estimate) && estimate > 0) {
394
- return Math.max(1, Math.floor(estimate));
395
- }
396
- return Number.POSITIVE_INFINITY;
397
- }
398
-
399
- function clampSimilarity(value: number): number {
400
- return Math.min(1, Math.max(-1, value));
401
- }
402
-
403
- function clampOpenUnit(value: number): number {
404
- return Math.min(0.999999, Math.max(0.000001, value));
405
- }
406
-
407
- function normalizeWeights(alpha: number, beta: number, gamma: number): { alpha: number; beta: number; gamma: number } {
408
- alpha = clamp01(alpha);
409
- beta = clamp01(beta);
410
- gamma = clamp01(gamma);
411
-
412
- const sum = alpha + beta + gamma;
413
- if (sum <= 0) {
414
- return { alpha: 0.7, beta: 0.2, gamma: 0.1 };
415
- }
416
-
417
- return {
418
- alpha: alpha / sum,
419
- beta: beta / sum,
420
- gamma: gamma / sum,
421
- };
422
- }
423
-
424
- function dedupeCandidates(items: SearchResult[]): SearchResult[] {
425
- const seen = new Set<string>();
426
- const out: SearchResult[] = [];
427
- for (const item of items) {
428
- const key = `${typeof item.metadata.collection === "string" ? item.metadata.collection : ""}::${item.id}`;
429
- if (seen.has(key)) {
430
- continue;
431
- }
432
- seen.add(key);
433
- out.push(item);
434
- }
435
- return out;
436
- }
437
-
438
- function similarity(item: SearchResult): number {
439
- return clampSimilarity(typeof item.score === "number" ? item.score : 0);
440
- }
441
-
442
- function accessCount(item: SearchResult): number {
443
- const raw = item.metadata.access_count;
444
- return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : 0;
445
- }
446
-
447
- function authorityWeight(
448
- item: SearchResult,
449
- opts: {
450
- now: number;
451
- authorityRecencyLambda: number;
452
- alphaR: number;
453
- alphaF: number;
454
- alphaA: number;
455
- maxAccessCount: number;
456
- },
457
- ): number {
458
- const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : opts.now;
459
- const ageSeconds = Math.max(0, opts.now - ts) / 1000;
460
- const recency = Math.exp(-opts.authorityRecencyLambda * ageSeconds);
461
- const frequency = normalizedFrequency(accessCount(item), opts.maxAccessCount);
462
- const authoredAuthority = clamp01(
463
- typeof item.metadata.authority === "number"
464
- ? item.metadata.authority
465
- : item.metadata.authored === true
466
- ? 1
467
- : 0,
468
- );
469
- return clamp01(
470
- opts.alphaR * recency +
471
- opts.alphaF * frequency +
472
- opts.alphaA * authoredAuthority,
473
- );
474
- }
475
-
476
- function normalizedFrequency(accessCount: number, maxAccessCount: number): number {
477
- if (accessCount <= 0 || maxAccessCount <= 0) {
478
- return 0;
479
- }
480
- return Math.log(1 + accessCount) / Math.log(1 + maxAccessCount + 1);
481
- }
482
-
483
- function computeRecencyScore(item: SearchResult, now: number, recencyLambda: number): number {
484
- const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : now;
485
- const ageSeconds = Math.max(0, now - ts) / 1000;
486
- return Math.exp(-recencyLambda * ageSeconds);
487
- }
488
-
489
- function buildRawUserRecoveryRationale(scores: {
490
- semanticScore: number;
491
- lexicalCoverage: number;
492
- recencyScore: number;
493
- intentAlignmentBonus: number;
494
- }): string {
495
- if (scores.intentAlignmentBonus >= 0.04) {
496
- return "intent phrase overlap lifted this candidate toward the query's direct ask";
497
- }
498
- const lexicalDelta = scores.lexicalCoverage - scores.semanticScore;
499
- if (lexicalDelta > 0.15) {
500
- return "lexical coverage lifted this candidate above its semantic score";
501
- }
502
- if (lexicalDelta < -0.15) {
503
- return "semantic similarity carried this candidate despite weaker lexical coverage";
504
- }
505
- if (scores.recencyScore > 0.9) {
506
- return "semantic and lexical scores were close; recency broke the tie";
507
- }
508
- return "semantic and lexical scores were balanced";
509
- }
510
-
511
- function computeIntentAlignmentBonus(text: string, intentPhrases: string[]): number {
512
- if (intentPhrases.length === 0) {
513
- return 0;
514
- }
515
- const normalized = normalizeTextForPhraseMatch(text);
516
- const matched = intentPhrases.filter((phrase) => normalized.includes(phrase)).length;
517
- if (matched === 0) {
518
- return 0;
519
- }
520
- // Keep this a small nudge so phrase overlap cannot overwhelm lexical precision.
521
- return Math.min(0.04, matched * 0.01);
522
- }
523
-
524
- function extractIntentPhrases(text: string): string[] {
525
- const terms = normalizeTerms(text).filter((term) => !INTENT_STOPWORDS.has(term));
526
- const phrases: string[] = [];
527
- for (let size = 4; size >= 2; size -= 1) {
528
- for (let i = 0; i <= terms.length - size; i += 1) {
529
- const phraseTerms = terms.slice(i, i + size);
530
- if (phraseTerms.some((term) => term.length < 3)) {
531
- continue;
532
- }
533
- const phrase = phraseTerms.join(" ");
534
- if (!phrases.includes(phrase)) {
535
- phrases.push(phrase);
536
- }
537
- }
538
- }
539
- return phrases.slice(0, 12);
540
- }
541
-
542
- function normalizeTextForPhraseMatch(text: string): string {
543
- return normalizeTerms(text).join(" ");
544
- }
545
-
546
- const INTENT_STOPWORDS = new Set([
547
- "the",
548
- "and",
549
- "for",
550
- "with",
551
- "that",
552
- "this",
553
- "have",
554
- "from",
555
- "your",
556
- "what",
557
- "when",
558
- "where",
559
- "which",
560
- "would",
561
- "could",
562
- "should",
563
- "about",
564
- "into",
565
- "some",
566
- "before",
567
- "after",
568
- "them",
569
- "they",
570
- "been",
571
- "just",
572
- "want",
573
- "looking",
574
- "look",
575
- "help",
576
- "need",
577
- "recommend",
578
- "suggestions",
579
- "suggest",
580
- "advice",
581
- "think",
582
- "also",
583
- ]);
584
-
585
- function extractKeywords(text: string): string[] {
586
- const tokens = normalizeTerms(text);
587
- const seen = new Set<string>();
588
- const keywords: string[] = [];
589
- for (const token of tokens) {
590
- if (token.length < 3 || seen.has(token)) {
591
- continue;
592
- }
593
- seen.add(token);
594
- keywords.push(token);
595
- }
596
- return keywords;
597
- }
598
-
599
- function normalizedKeywordCoverage(keywords: string[], text: string): number {
600
- if (keywords.length === 0) {
601
- return 0;
602
- }
603
- const docTerms = new Set(normalizeTerms(text));
604
- let matches = 0;
605
- for (const keyword of keywords) {
606
- if (docTerms.has(keyword)) {
607
- matches += 1;
608
- }
609
- }
610
- return matches / Math.max(keywords.length, 1);
611
- }
612
-
613
- function normalizeTerms(text: string): string[] {
614
- return text
615
- .toLowerCase()
616
- .split(/[^a-z0-9_]+/i)
617
- .filter((term) => term.length > 0);
618
- }
619
-
620
- function hopTargets(item: SearchResult): string[] {
621
- const raw = item.metadata.hop_targets;
622
- if (Array.isArray(raw)) {
623
- return raw.filter((target): target is string => typeof target === "string" && target.length > 0);
624
- }
625
- if (typeof raw === "string") {
626
- return raw
627
- .split(",")
628
- .map((part) => part.trim())
629
- .filter((part) => part.length > 0);
630
- }
631
- return [];
632
- }