@agenr/agenr-plugin 1.7.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-6CEKKEFZ.js +4954 -0
- package/dist/chunk-ETQPUJGS.js +0 -0
- package/dist/chunk-GUDCFFRV.js +1517 -0
- package/dist/chunk-LVDQXSHP.js +5122 -0
- package/dist/index.js +334 -261
- package/openclaw.plugin.json +31 -8
- package/package.json +2 -2
- package/dist/chunk-7WL5EAQZ.js +0 -758
|
@@ -0,0 +1,1517 @@
|
|
|
1
|
+
// src/core/recall/scoring.ts
|
|
2
|
+
var DAY_IN_MILLISECONDS = 1e3 * 60 * 60 * 24;
|
|
3
|
+
var IMPORTANCE_FLOOR = 0.4;
|
|
4
|
+
var RELEVANCE_WEIGHT = 0.5;
|
|
5
|
+
var RECENCY_WEIGHT = 0.25;
|
|
6
|
+
var IMPORTANCE_WEIGHT = 0.25;
|
|
7
|
+
function recencyScore(createdAt, expiry, now = /* @__PURE__ */ new Date()) {
|
|
8
|
+
if (expiry === "core") {
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const createdDate = asValidDate(createdAt);
|
|
12
|
+
const nowDate = asValidDate(now);
|
|
13
|
+
if (!createdDate || !nowDate) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const halfLifeDays = expiry === "permanent" ? 365 : 30;
|
|
17
|
+
const daysOld = Math.max(0, (nowDate.getTime() - createdDate.getTime()) / DAY_IN_MILLISECONDS);
|
|
18
|
+
return clampUnit(Math.pow(0.5, daysOld / halfLifeDays));
|
|
19
|
+
}
|
|
20
|
+
function gaussianRecency(createdAt, aroundDate, radiusDays) {
|
|
21
|
+
const createdDate = asValidDate(createdAt);
|
|
22
|
+
const anchorDate = asValidDate(aroundDate);
|
|
23
|
+
const normalizedRadius = sanitizeNonNegative(radiusDays);
|
|
24
|
+
if (!createdDate || !anchorDate) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (normalizedRadius <= 0) {
|
|
28
|
+
return createdDate.getTime() === anchorDate.getTime() ? 1 : 0;
|
|
29
|
+
}
|
|
30
|
+
const daysDelta = Math.abs(createdDate.getTime() - anchorDate.getTime()) / DAY_IN_MILLISECONDS;
|
|
31
|
+
return clampUnit(Math.exp(-0.5 * (daysDelta / normalizedRadius) ** 2));
|
|
32
|
+
}
|
|
33
|
+
function importanceScore(importance) {
|
|
34
|
+
const clampedImportance = clampRange(sanitizeNonNegative(importance), 1, 10);
|
|
35
|
+
return clampUnit(IMPORTANCE_FLOOR + (clampedImportance - 1) / 9 * (1 - IMPORTANCE_FLOOR));
|
|
36
|
+
}
|
|
37
|
+
function combinedRelevance(vectorSim, lexical) {
|
|
38
|
+
const normalizedVector = clampUnit(sanitizeNonNegative(vectorSim));
|
|
39
|
+
const normalizedLexical = clampUnit(sanitizeNonNegative(lexical));
|
|
40
|
+
if (normalizedVector > 0 && normalizedLexical > 0) {
|
|
41
|
+
return clampUnit(normalizedVector * 0.6 + normalizedLexical * 0.4);
|
|
42
|
+
}
|
|
43
|
+
return Math.max(normalizedVector, normalizedLexical);
|
|
44
|
+
}
|
|
45
|
+
function scoreCandidate(params) {
|
|
46
|
+
const vector = clampUnit(sanitizeNonNegative(params.vectorSim));
|
|
47
|
+
const lexical = clampUnit(sanitizeNonNegative(params.lexical));
|
|
48
|
+
const recency = clampUnit(sanitizeNonNegative(params.recency));
|
|
49
|
+
const importance = clampUnit(sanitizeNonNegative(params.importance));
|
|
50
|
+
const relevance = combinedRelevance(vector, lexical);
|
|
51
|
+
const score = clampUnit(relevance * RELEVANCE_WEIGHT + recency * RECENCY_WEIGHT + importance * IMPORTANCE_WEIGHT);
|
|
52
|
+
return {
|
|
53
|
+
score,
|
|
54
|
+
scores: {
|
|
55
|
+
relevance,
|
|
56
|
+
vector,
|
|
57
|
+
lexical,
|
|
58
|
+
recency,
|
|
59
|
+
importance
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function cosineSimilarity(left, right) {
|
|
64
|
+
const size = Math.min(left.length, right.length);
|
|
65
|
+
if (size === 0) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
let dot = 0;
|
|
69
|
+
let leftNorm = 0;
|
|
70
|
+
let rightNorm = 0;
|
|
71
|
+
for (let index = 0; index < size; index += 1) {
|
|
72
|
+
const leftValue = sanitizeFinite(left[index]);
|
|
73
|
+
const rightValue = sanitizeFinite(right[index]);
|
|
74
|
+
dot += leftValue * rightValue;
|
|
75
|
+
leftNorm += leftValue * leftValue;
|
|
76
|
+
rightNorm += rightValue * rightValue;
|
|
77
|
+
}
|
|
78
|
+
if (leftNorm <= 0 || rightNorm <= 0) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return clampUnit(dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm)));
|
|
82
|
+
}
|
|
83
|
+
var asValidDate = (value) => {
|
|
84
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
85
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
86
|
+
};
|
|
87
|
+
var clampUnit = (value) => clampRange(sanitizeNonNegative(value), 0, 1);
|
|
88
|
+
var clampRange = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
89
|
+
var sanitizeFinite = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
90
|
+
var sanitizeNonNegative = (value) => Math.max(0, sanitizeFinite(value));
|
|
91
|
+
|
|
92
|
+
// src/core/recall/temporal.ts
|
|
93
|
+
var DAY_IN_MILLISECONDS2 = 1e3 * 60 * 60 * 24;
|
|
94
|
+
var MONTH_INDEX = /* @__PURE__ */ new Map([
|
|
95
|
+
["january", 0],
|
|
96
|
+
["february", 1],
|
|
97
|
+
["march", 2],
|
|
98
|
+
["april", 3],
|
|
99
|
+
["may", 4],
|
|
100
|
+
["june", 5],
|
|
101
|
+
["july", 6],
|
|
102
|
+
["august", 7],
|
|
103
|
+
["september", 8],
|
|
104
|
+
["october", 9],
|
|
105
|
+
["november", 10],
|
|
106
|
+
["december", 11]
|
|
107
|
+
]);
|
|
108
|
+
function inferAroundDate(text, now = /* @__PURE__ */ new Date()) {
|
|
109
|
+
const normalized = text.trim().toLowerCase();
|
|
110
|
+
const referenceNow = asValidDate2(now);
|
|
111
|
+
if (normalized.length === 0 || !referenceNow) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let inferred = null;
|
|
115
|
+
if (/\byesterday\b/.test(normalized)) {
|
|
116
|
+
inferred = offsetDays(referenceNow, 1);
|
|
117
|
+
} else if (/\blast week\b/.test(normalized)) {
|
|
118
|
+
inferred = offsetDays(referenceNow, 7);
|
|
119
|
+
} else if (/\blast month\b/.test(normalized)) {
|
|
120
|
+
inferred = offsetDays(referenceNow, 30);
|
|
121
|
+
} else if (/\blast year\b/.test(normalized)) {
|
|
122
|
+
inferred = offsetDays(referenceNow, 365);
|
|
123
|
+
} else if (/\bthis week\b/.test(normalized)) {
|
|
124
|
+
inferred = offsetDays(referenceNow, 3);
|
|
125
|
+
} else if (/\bthis month\b/.test(normalized)) {
|
|
126
|
+
inferred = offsetDays(referenceNow, 15);
|
|
127
|
+
} else {
|
|
128
|
+
const relativeMatch = normalized.match(/\b(\d+)\s+(day|days|week|weeks|month|months)\s+ago\b/);
|
|
129
|
+
if (relativeMatch) {
|
|
130
|
+
const amount = Number(relativeMatch[1]);
|
|
131
|
+
const unit = relativeMatch[2];
|
|
132
|
+
const multiplier = unit?.startsWith("week") ? 7 : unit?.startsWith("month") ? 30 : 1;
|
|
133
|
+
inferred = Number.isFinite(amount) ? offsetDays(referenceNow, amount * multiplier) : null;
|
|
134
|
+
} else {
|
|
135
|
+
const monthMatch = normalized.match(/\bin\s+(january|february|march|april|may|june|july|august|september|october|november|december)\b/);
|
|
136
|
+
if (monthMatch?.[1]) {
|
|
137
|
+
inferred = inferMonthAnchor(monthMatch[1], referenceNow);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!inferred) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return inferred.getTime() > referenceNow.getTime() ? new Date(referenceNow.getTime()) : inferred;
|
|
145
|
+
}
|
|
146
|
+
function parseRelativeDate(input, now = /* @__PURE__ */ new Date()) {
|
|
147
|
+
const trimmed = input.trim();
|
|
148
|
+
const referenceNow = asValidDate2(now);
|
|
149
|
+
if (trimmed.length === 0 || !referenceNow) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const durationMatch = trimmed.match(/^(\d+)d$/i);
|
|
153
|
+
if (durationMatch?.[1]) {
|
|
154
|
+
const days = Number(durationMatch[1]);
|
|
155
|
+
return Number.isFinite(days) ? offsetDays(referenceNow, days) : null;
|
|
156
|
+
}
|
|
157
|
+
const parsed = new Date(trimmed);
|
|
158
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
159
|
+
}
|
|
160
|
+
var asValidDate2 = (value) => {
|
|
161
|
+
const date = new Date(value.getTime());
|
|
162
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
163
|
+
};
|
|
164
|
+
var offsetDays = (date, days) => new Date(date.getTime() - days * DAY_IN_MILLISECONDS2);
|
|
165
|
+
var inferMonthAnchor = (monthName, now) => {
|
|
166
|
+
const monthIndex = MONTH_INDEX.get(monthName);
|
|
167
|
+
if (monthIndex === void 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const year = monthIndex <= now.getUTCMonth() ? now.getUTCFullYear() : now.getUTCFullYear() - 1;
|
|
171
|
+
return new Date(Date.UTC(year, monthIndex, 15));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/core/recall/lexical.ts
|
|
175
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
176
|
+
"the",
|
|
177
|
+
"a",
|
|
178
|
+
"an",
|
|
179
|
+
"is",
|
|
180
|
+
"are",
|
|
181
|
+
"was",
|
|
182
|
+
"were",
|
|
183
|
+
"be",
|
|
184
|
+
"been",
|
|
185
|
+
"being",
|
|
186
|
+
"have",
|
|
187
|
+
"has",
|
|
188
|
+
"had",
|
|
189
|
+
"do",
|
|
190
|
+
"does",
|
|
191
|
+
"did",
|
|
192
|
+
"will",
|
|
193
|
+
"would",
|
|
194
|
+
"could",
|
|
195
|
+
"should",
|
|
196
|
+
"may",
|
|
197
|
+
"might",
|
|
198
|
+
"shall",
|
|
199
|
+
"can",
|
|
200
|
+
"need",
|
|
201
|
+
"must",
|
|
202
|
+
"i",
|
|
203
|
+
"me",
|
|
204
|
+
"my",
|
|
205
|
+
"we",
|
|
206
|
+
"our",
|
|
207
|
+
"you",
|
|
208
|
+
"your",
|
|
209
|
+
"he",
|
|
210
|
+
"him",
|
|
211
|
+
"his",
|
|
212
|
+
"she",
|
|
213
|
+
"her",
|
|
214
|
+
"it",
|
|
215
|
+
"its",
|
|
216
|
+
"they",
|
|
217
|
+
"them",
|
|
218
|
+
"their",
|
|
219
|
+
"this",
|
|
220
|
+
"that",
|
|
221
|
+
"these",
|
|
222
|
+
"those",
|
|
223
|
+
"what",
|
|
224
|
+
"which",
|
|
225
|
+
"who",
|
|
226
|
+
"whom",
|
|
227
|
+
"in",
|
|
228
|
+
"on",
|
|
229
|
+
"at",
|
|
230
|
+
"to",
|
|
231
|
+
"for",
|
|
232
|
+
"of",
|
|
233
|
+
"with",
|
|
234
|
+
"by",
|
|
235
|
+
"from",
|
|
236
|
+
"up",
|
|
237
|
+
"about",
|
|
238
|
+
"into",
|
|
239
|
+
"through",
|
|
240
|
+
"during",
|
|
241
|
+
"before",
|
|
242
|
+
"after",
|
|
243
|
+
"above",
|
|
244
|
+
"below",
|
|
245
|
+
"and",
|
|
246
|
+
"or",
|
|
247
|
+
"but",
|
|
248
|
+
"not",
|
|
249
|
+
"no",
|
|
250
|
+
"nor",
|
|
251
|
+
"so",
|
|
252
|
+
"if",
|
|
253
|
+
"then",
|
|
254
|
+
"else",
|
|
255
|
+
"when",
|
|
256
|
+
"where",
|
|
257
|
+
"how",
|
|
258
|
+
"all",
|
|
259
|
+
"each",
|
|
260
|
+
"every",
|
|
261
|
+
"both",
|
|
262
|
+
"few",
|
|
263
|
+
"more",
|
|
264
|
+
"some",
|
|
265
|
+
"any",
|
|
266
|
+
"other",
|
|
267
|
+
"than"
|
|
268
|
+
]);
|
|
269
|
+
var FTS_OPERATOR_TOKENS = /* @__PURE__ */ new Set(["or", "not", "near"]);
|
|
270
|
+
var LEXICAL_TOKEN_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}._-]*/gu;
|
|
271
|
+
function tokenize(text) {
|
|
272
|
+
const matches = normalizeLexicalText(text).match(LEXICAL_TOKEN_PATTERN) ?? [];
|
|
273
|
+
return matches.filter((token) => token.length >= 2 && !STOP_WORDS.has(token));
|
|
274
|
+
}
|
|
275
|
+
function buildLexicalPlan(text) {
|
|
276
|
+
const trimmed = text.trim();
|
|
277
|
+
if (trimmed.length === 0) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const tokens = tokenize(trimmed).filter((token) => !FTS_OPERATOR_TOKENS.has(token));
|
|
281
|
+
if (tokens.length === 0) {
|
|
282
|
+
return [
|
|
283
|
+
{
|
|
284
|
+
tier: "exact",
|
|
285
|
+
text: trimmed
|
|
286
|
+
}
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
if (tokens.length === 1) {
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
tier: "exact",
|
|
293
|
+
text: trimmed
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
tier: "all_tokens",
|
|
297
|
+
tokens
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
{
|
|
303
|
+
tier: "exact",
|
|
304
|
+
text: trimmed
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
tier: "all_tokens",
|
|
308
|
+
tokens
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
tier: "any_tokens",
|
|
312
|
+
tokens
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
function computeLexicalScore(query, subject, content) {
|
|
317
|
+
const queryTokens = tokenize(query);
|
|
318
|
+
const subjectTokens = tokenize(subject);
|
|
319
|
+
const contentTokens = tokenize(content);
|
|
320
|
+
const subjectTokenSet = new Set(subjectTokens);
|
|
321
|
+
const contentTokenSet = new Set(contentTokens);
|
|
322
|
+
const tokenOverlap = queryTokens.length === 0 ? 0 : queryTokens.filter((token) => subjectTokenSet.has(token) || contentTokenSet.has(token)).length / queryTokens.length;
|
|
323
|
+
const phraseMatches = countPhraseMatches(queryTokens, subjectTokens, contentTokens);
|
|
324
|
+
const phraseBonus = Math.min(0.4, phraseMatches * 0.2);
|
|
325
|
+
const subjectBonus = normalizeText(query) === normalizeText(subject) && normalizeText(query).length > 0 ? 0.3 : 0;
|
|
326
|
+
return Math.min(1, tokenOverlap + phraseBonus + subjectBonus);
|
|
327
|
+
}
|
|
328
|
+
var normalizeText = (text) => normalizeLexicalText(text).trim();
|
|
329
|
+
var normalizeLexicalText = (text) => text.normalize("NFKC").toLocaleLowerCase();
|
|
330
|
+
var countPhraseMatches = (queryTokens, subjectTokens, contentTokens) => {
|
|
331
|
+
if (queryTokens.length < 2) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
const matchedPhrases = /* @__PURE__ */ new Set();
|
|
335
|
+
for (let size = 2; size <= queryTokens.length; size += 1) {
|
|
336
|
+
for (let index = 0; index + size <= queryTokens.length; index += 1) {
|
|
337
|
+
const phraseTokens = queryTokens.slice(index, index + size);
|
|
338
|
+
if (hasConsecutivePhrase(subjectTokens, phraseTokens) || hasConsecutivePhrase(contentTokens, phraseTokens)) {
|
|
339
|
+
matchedPhrases.add(phraseTokens.join(" "));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return matchedPhrases.size;
|
|
344
|
+
};
|
|
345
|
+
var hasConsecutivePhrase = (haystack, needle) => {
|
|
346
|
+
if (needle.length === 0 || haystack.length < needle.length) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
for (let index = 0; index + needle.length <= haystack.length; index += 1) {
|
|
350
|
+
let matches = true;
|
|
351
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
352
|
+
if (haystack[index + offset] !== needle[offset]) {
|
|
353
|
+
matches = false;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (matches) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// src/core/claim-key.ts
|
|
365
|
+
var UNKNOWN_SEGMENT = "unknown";
|
|
366
|
+
var SELF_REFERENTIAL_ENTITIES = /* @__PURE__ */ new Set(["i", "me", "myself", "the_user", "user", "we", "our_team", "the_project", "this_project"]);
|
|
367
|
+
var GENERIC_ENTITIES = /* @__PURE__ */ new Set([
|
|
368
|
+
"app",
|
|
369
|
+
"company",
|
|
370
|
+
"config",
|
|
371
|
+
"data",
|
|
372
|
+
"device",
|
|
373
|
+
"entity",
|
|
374
|
+
"environment",
|
|
375
|
+
"item",
|
|
376
|
+
"organization",
|
|
377
|
+
"person",
|
|
378
|
+
"place",
|
|
379
|
+
"project",
|
|
380
|
+
"service",
|
|
381
|
+
"setting",
|
|
382
|
+
"system",
|
|
383
|
+
"team",
|
|
384
|
+
"thing",
|
|
385
|
+
"user",
|
|
386
|
+
"workspace"
|
|
387
|
+
]);
|
|
388
|
+
var GENERIC_ATTRIBUTES = /* @__PURE__ */ new Set(["info", "details", "config", "stuff", "thing", "data"]);
|
|
389
|
+
var COMPACTION_RELATION_TOKENS = /* @__PURE__ */ new Set([
|
|
390
|
+
"after",
|
|
391
|
+
"before",
|
|
392
|
+
"depend",
|
|
393
|
+
"depends",
|
|
394
|
+
"follows",
|
|
395
|
+
"follow",
|
|
396
|
+
"keep",
|
|
397
|
+
"keeps",
|
|
398
|
+
"maintain",
|
|
399
|
+
"maintains",
|
|
400
|
+
"need",
|
|
401
|
+
"needs",
|
|
402
|
+
"precede",
|
|
403
|
+
"precedes",
|
|
404
|
+
"preserve",
|
|
405
|
+
"preserves",
|
|
406
|
+
"require",
|
|
407
|
+
"required",
|
|
408
|
+
"requires",
|
|
409
|
+
"retain",
|
|
410
|
+
"retains"
|
|
411
|
+
]);
|
|
412
|
+
var COMPACTION_BREAK_TOKENS = /* @__PURE__ */ new Set(["about", "across", "and", "between", "during", "for", "from", "into", "onto", "or", "to", "with"]);
|
|
413
|
+
var COMPACTION_WEAK_LEADING_TOKENS = /* @__PURE__ */ new Set(["actual", "authoritative", "canonical", "concrete", "current", "durable", "existing", "real"]);
|
|
414
|
+
var ACTION_CONDITION_TOKENS = /* @__PURE__ */ new Set(["activate", "activation", "apply", "fire", "launch", "run", "start", "trigger"]);
|
|
415
|
+
var TRAILING_OBJECT_COMPACTION_PREPOSITIONS = /* @__PURE__ */ new Set(["about", "for", "from", "into", "onto", "to", "with"]);
|
|
416
|
+
var TRAILING_OBJECT_TRANSFER_HEADS = /* @__PURE__ */ new Set([
|
|
417
|
+
"access",
|
|
418
|
+
"boundary",
|
|
419
|
+
"condition",
|
|
420
|
+
"contract",
|
|
421
|
+
"guide",
|
|
422
|
+
"path",
|
|
423
|
+
"policy",
|
|
424
|
+
"preference",
|
|
425
|
+
"process",
|
|
426
|
+
"rule",
|
|
427
|
+
"schedule",
|
|
428
|
+
"support",
|
|
429
|
+
"surface",
|
|
430
|
+
"window",
|
|
431
|
+
"workflow"
|
|
432
|
+
]);
|
|
433
|
+
var STABLE_ATTRIBUTE_HEADS = /* @__PURE__ */ new Set([
|
|
434
|
+
"access",
|
|
435
|
+
"boundary",
|
|
436
|
+
"condition",
|
|
437
|
+
"contract",
|
|
438
|
+
"default",
|
|
439
|
+
"dependency",
|
|
440
|
+
"guide",
|
|
441
|
+
"mode",
|
|
442
|
+
"order",
|
|
443
|
+
"path",
|
|
444
|
+
"policy",
|
|
445
|
+
"preference",
|
|
446
|
+
"preservation",
|
|
447
|
+
"process",
|
|
448
|
+
"requirement",
|
|
449
|
+
"rule",
|
|
450
|
+
"schedule",
|
|
451
|
+
"setting",
|
|
452
|
+
"status",
|
|
453
|
+
"strategy",
|
|
454
|
+
"support",
|
|
455
|
+
"surface",
|
|
456
|
+
"timezone",
|
|
457
|
+
"truth",
|
|
458
|
+
"version",
|
|
459
|
+
"window",
|
|
460
|
+
"workflow"
|
|
461
|
+
]);
|
|
462
|
+
function normalizeClaimKeySegment(value) {
|
|
463
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
464
|
+
}
|
|
465
|
+
function normalizeClaimKey(value) {
|
|
466
|
+
const trimmed = value.trim();
|
|
467
|
+
if (trimmed.length === 0) {
|
|
468
|
+
return { ok: false, reason: "empty" };
|
|
469
|
+
}
|
|
470
|
+
const slashCount = Array.from(trimmed).filter((character) => character === "/").length;
|
|
471
|
+
if (slashCount === 0) {
|
|
472
|
+
return { ok: false, reason: "missing_separator" };
|
|
473
|
+
}
|
|
474
|
+
if (slashCount !== 1) {
|
|
475
|
+
return { ok: false, reason: "too_many_segments" };
|
|
476
|
+
}
|
|
477
|
+
const [rawEntity = "", rawAttribute = ""] = trimmed.split("/");
|
|
478
|
+
const entity = normalizeClaimKeySegment(rawEntity);
|
|
479
|
+
if (entity.length === 0) {
|
|
480
|
+
return { ok: false, reason: "empty_entity" };
|
|
481
|
+
}
|
|
482
|
+
const attribute = normalizeClaimKeySegment(rawAttribute);
|
|
483
|
+
if (attribute.length === 0) {
|
|
484
|
+
return { ok: false, reason: "empty_attribute" };
|
|
485
|
+
}
|
|
486
|
+
if (entity === UNKNOWN_SEGMENT && attribute === UNKNOWN_SEGMENT) {
|
|
487
|
+
return { ok: false, reason: "unknown_pair" };
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: {
|
|
492
|
+
claimKey: `${entity}/${attribute}`,
|
|
493
|
+
entity,
|
|
494
|
+
attribute
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function compactClaimKey(claimKey) {
|
|
499
|
+
const normalized = normalizeClaimKey(claimKey);
|
|
500
|
+
if (!normalized.ok) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
let attributeTokens = normalized.value.attribute.split("_").filter((token) => token.length > 0);
|
|
504
|
+
const entityTokens = normalized.value.entity.split("_").filter((token) => token.length > 0);
|
|
505
|
+
const reasons = [];
|
|
506
|
+
if (entityTokens.length > 0 && startsWithTokens(attributeTokens, entityTokens) && attributeTokens.length > entityTokens.length) {
|
|
507
|
+
attributeTokens = attributeTokens.slice(entityTokens.length);
|
|
508
|
+
reasons.push("removed duplicated entity prefix from attribute");
|
|
509
|
+
}
|
|
510
|
+
if (entityTokens.length > 0 && attributeTokens.length > entityTokens.length + 1 && endsWithTokens(attributeTokens, entityTokens) && TRAILING_OBJECT_COMPACTION_PREPOSITIONS.has(attributeTokens[attributeTokens.length - entityTokens.length - 1] ?? "")) {
|
|
511
|
+
attributeTokens = attributeTokens.slice(0, attributeTokens.length - entityTokens.length - 1);
|
|
512
|
+
reasons.push("removed duplicated entity suffix from attribute");
|
|
513
|
+
}
|
|
514
|
+
const sourceOfTruthCompaction = compactSourceOfTruthAttribute(attributeTokens);
|
|
515
|
+
if (sourceOfTruthCompaction) {
|
|
516
|
+
attributeTokens = sourceOfTruthCompaction.attributeTokens;
|
|
517
|
+
reasons.push(sourceOfTruthCompaction.reason);
|
|
518
|
+
} else {
|
|
519
|
+
const relationCompaction = compactRelationAttribute(attributeTokens);
|
|
520
|
+
if (relationCompaction) {
|
|
521
|
+
attributeTokens = relationCompaction.attributeTokens;
|
|
522
|
+
reasons.push(relationCompaction.reason);
|
|
523
|
+
} else {
|
|
524
|
+
const trailingObjectCompaction = compactTrailingObjectAttribute(attributeTokens);
|
|
525
|
+
if (trailingObjectCompaction) {
|
|
526
|
+
attributeTokens = trailingObjectCompaction.attributeTokens;
|
|
527
|
+
reasons.push(trailingObjectCompaction.reason);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const attribute = attributeTokens.join("_");
|
|
532
|
+
if (attribute.length === 0) {
|
|
533
|
+
return {
|
|
534
|
+
claimKey: normalized.value.claimKey,
|
|
535
|
+
entity: normalized.value.entity,
|
|
536
|
+
attribute: normalized.value.attribute,
|
|
537
|
+
compactedFrom: null,
|
|
538
|
+
reason: null
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const compactedClaimKey = `${normalized.value.entity}/${attribute}`;
|
|
542
|
+
return {
|
|
543
|
+
claimKey: compactedClaimKey,
|
|
544
|
+
entity: normalized.value.entity,
|
|
545
|
+
attribute,
|
|
546
|
+
compactedFrom: compactedClaimKey !== normalized.value.claimKey ? normalized.value.claimKey : null,
|
|
547
|
+
reason: reasons.length > 0 ? joinCompactionReasons(reasons) : null
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function validateExtractedClaimKey(claimKey) {
|
|
551
|
+
if (SELF_REFERENTIAL_ENTITIES.has(claimKey.entity)) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
reason: "self_referential_entity",
|
|
555
|
+
value: claimKey
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (GENERIC_ATTRIBUTES.has(claimKey.attribute)) {
|
|
559
|
+
return {
|
|
560
|
+
ok: false,
|
|
561
|
+
reason: "generic_attribute",
|
|
562
|
+
value: claimKey
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (isValueShapedAttribute(claimKey.attribute)) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
reason: "value_shaped_attribute",
|
|
569
|
+
value: claimKey
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
ok: true,
|
|
574
|
+
value: claimKey
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function inspectClaimKey(value) {
|
|
578
|
+
const rawClaimKey = value.trim();
|
|
579
|
+
const normalized = normalizeClaimKey(rawClaimKey);
|
|
580
|
+
if (!normalized.ok) {
|
|
581
|
+
return {
|
|
582
|
+
rawClaimKey,
|
|
583
|
+
canonical: false,
|
|
584
|
+
normalizationFailure: normalized.reason,
|
|
585
|
+
suspectReasons: []
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const suspectReasons = /* @__PURE__ */ new Set();
|
|
589
|
+
const validation = validateExtractedClaimKey(normalized.value);
|
|
590
|
+
if (!validation.ok) {
|
|
591
|
+
suspectReasons.add(validation.reason);
|
|
592
|
+
}
|
|
593
|
+
if (GENERIC_ENTITIES.has(normalized.value.entity)) {
|
|
594
|
+
suspectReasons.add("generic_entity");
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
rawClaimKey,
|
|
598
|
+
canonical: normalized.value.claimKey === rawClaimKey,
|
|
599
|
+
normalized: normalized.value,
|
|
600
|
+
suspectReasons: [...suspectReasons]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function isTrustedClaimKeyForCleanup(value) {
|
|
604
|
+
const inspection = inspectClaimKey(value);
|
|
605
|
+
return Boolean(inspection.canonical && inspection.normalized && inspection.suspectReasons.length === 0);
|
|
606
|
+
}
|
|
607
|
+
function describeClaimKeyNormalizationFailure(reason) {
|
|
608
|
+
switch (reason) {
|
|
609
|
+
case "empty":
|
|
610
|
+
return "claim key was empty";
|
|
611
|
+
case "missing_separator":
|
|
612
|
+
return "claim key must contain exactly one '/'";
|
|
613
|
+
case "too_many_segments":
|
|
614
|
+
return "claim key must contain exactly one '/'";
|
|
615
|
+
case "empty_entity":
|
|
616
|
+
return "claim key entity was empty after normalization";
|
|
617
|
+
case "empty_attribute":
|
|
618
|
+
return "claim key attribute was empty after normalization";
|
|
619
|
+
case "unknown_pair":
|
|
620
|
+
return 'claim key "unknown/unknown" is not allowed';
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function describeExtractedClaimKeyRejection(reason, claimKey) {
|
|
624
|
+
switch (reason) {
|
|
625
|
+
case "self_referential_entity":
|
|
626
|
+
return `entity "${claimKey.entity}" is self-referential`;
|
|
627
|
+
case "generic_attribute":
|
|
628
|
+
return `attribute "${claimKey.attribute}" is too generic`;
|
|
629
|
+
case "value_shaped_attribute":
|
|
630
|
+
return `attribute "${claimKey.attribute}" looks value-shaped`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function describeClaimKeySuspicion(reason, claimKey) {
|
|
634
|
+
switch (reason) {
|
|
635
|
+
case "generic_entity":
|
|
636
|
+
return `entity "${claimKey.entity}" is too generic`;
|
|
637
|
+
case "self_referential_entity":
|
|
638
|
+
case "generic_attribute":
|
|
639
|
+
case "value_shaped_attribute":
|
|
640
|
+
return describeExtractedClaimKeyRejection(reason, claimKey);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function isValueShapedAttribute(attribute) {
|
|
644
|
+
return /^\d+(?:_\d+)*$/u.test(attribute) || /^v\d+(?:_\d+)*$/u.test(attribute);
|
|
645
|
+
}
|
|
646
|
+
function compactSourceOfTruthAttribute(attributeTokens) {
|
|
647
|
+
const sourceOfTruthIndex = findSourceOfTruthPhraseIndex(attributeTokens);
|
|
648
|
+
if (sourceOfTruthIndex === -1) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const normalizedPhrase = ["source", "of", "truth"];
|
|
652
|
+
if (attributeTokens.length === normalizedPhrase.length && startsWithTokens(attributeTokens, normalizedPhrase)) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const before = attributeTokens.slice(0, sourceOfTruthIndex);
|
|
656
|
+
const after = attributeTokens.slice(sourceOfTruthIndex + normalizedPhrase.length);
|
|
657
|
+
const leadingAllowed = before.every((token) => COMPACTION_WEAK_LEADING_TOKENS.has(token));
|
|
658
|
+
const hasMixedStableFamily = before.some((token) => STABLE_ATTRIBUTE_HEADS.has(token)) || after.some((token) => STABLE_ATTRIBUTE_HEADS.has(token));
|
|
659
|
+
const hasConjunctionNoise = before.includes("and") || before.includes("or") || after.includes("and") || after.includes("or");
|
|
660
|
+
if (!leadingAllowed || hasMixedStableFamily || hasConjunctionNoise) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
attributeTokens: normalizedPhrase,
|
|
665
|
+
reason: "collapsed source-of-truth phrasing into the stable canonical slot"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function compactRelationAttribute(attributeTokens) {
|
|
669
|
+
const relationIndex = attributeTokens.findIndex((token) => COMPACTION_RELATION_TOKENS.has(token));
|
|
670
|
+
if (relationIndex === -1) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const relation = attributeTokens[relationIndex] ?? "";
|
|
674
|
+
const left = attributeTokens.slice(0, relationIndex);
|
|
675
|
+
const right = attributeTokens.slice(relationIndex + 1);
|
|
676
|
+
if (left.length === 0 && right.length === 0) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
if (isRequirementRelation(relation)) {
|
|
680
|
+
const conditionAction = extractConditionAction(right);
|
|
681
|
+
if (conditionAction) {
|
|
682
|
+
return {
|
|
683
|
+
attributeTokens: [conditionAction, "condition"],
|
|
684
|
+
reason: `collapsed a sentence-like ${conditionAction} requirement into a stable condition slot`
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const requirementFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
688
|
+
if (!requirementFocus) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
attributeTokens: [...requirementFocus, "requirement"],
|
|
693
|
+
reason: "collapsed a sentence-like requirement phrase into a stable requirement slot"
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (isOrderingRelation(relation)) {
|
|
697
|
+
const orderingFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
698
|
+
if (!orderingFocus) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
attributeTokens: [...orderingFocus, "order"],
|
|
703
|
+
reason: "collapsed a sentence-like ordering phrase into a stable order slot"
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (isPreservationRelation(relation)) {
|
|
707
|
+
const preservationFocus = extractCompactionFocus(right, 2) ?? extractCompactionFocus(left, 2);
|
|
708
|
+
if (!preservationFocus) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
attributeTokens: [...preservationFocus, "preservation"],
|
|
713
|
+
reason: "collapsed a sentence-like preservation phrase into a stable preservation slot"
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
function compactTrailingObjectAttribute(attributeTokens) {
|
|
719
|
+
const prepositionIndex = attributeTokens.findIndex((token) => TRAILING_OBJECT_COMPACTION_PREPOSITIONS.has(token));
|
|
720
|
+
if (prepositionIndex <= 0 || prepositionIndex >= attributeTokens.length - 1) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const left = trimWeakLeadingTokens(attributeTokens.slice(0, prepositionIndex));
|
|
724
|
+
const right = attributeTokens.slice(prepositionIndex + 1);
|
|
725
|
+
if (left.length === 0 || left.length > 3 || left.includes("and") || left.includes("or") || left.some((token) => COMPACTION_RELATION_TOKENS.has(token))) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const head = left[left.length - 1];
|
|
729
|
+
if (!head || !TRAILING_OBJECT_TRANSFER_HEADS.has(head)) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const objectFocus = extractCompactionFocus(right, 2);
|
|
733
|
+
if (!objectFocus) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const headCore = extractStableHeadCore(left, 2);
|
|
737
|
+
if (!headCore) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
attributeTokens: [...objectFocus, ...headCore],
|
|
742
|
+
reason: "collapsed a trailing object phrase into a compact stable slot name"
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function findSourceOfTruthPhraseIndex(tokens) {
|
|
746
|
+
for (let index = 0; index <= tokens.length - 3; index += 1) {
|
|
747
|
+
if (tokens[index] === "source" && tokens[index + 1] === "of" && tokens[index + 2] === "truth") {
|
|
748
|
+
return index;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return -1;
|
|
752
|
+
}
|
|
753
|
+
function extractConditionAction(tokens) {
|
|
754
|
+
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
755
|
+
const token = tokens[index];
|
|
756
|
+
if (token && ACTION_CONDITION_TOKENS.has(token)) {
|
|
757
|
+
return token;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
function extractCompactionFocus(tokens, limit) {
|
|
763
|
+
const compactable = trimWeakLeadingTokens(tokens).filter((token) => token.length > 0);
|
|
764
|
+
const segments = splitTokensOnBreaks(compactable).filter((segment) => segment.length > 0);
|
|
765
|
+
const preferredSegment = segments[0];
|
|
766
|
+
if (!preferredSegment || preferredSegment.length === 0) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
return preferredSegment.slice(0, limit);
|
|
770
|
+
}
|
|
771
|
+
function extractStableHeadCore(tokens, limit) {
|
|
772
|
+
const compactable = trimWeakLeadingTokens(tokens).filter((token) => token.length > 0);
|
|
773
|
+
const head = compactable[compactable.length - 1];
|
|
774
|
+
if (!head || !STABLE_ATTRIBUTE_HEADS.has(head)) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
return compactable.slice(Math.max(0, compactable.length - limit));
|
|
778
|
+
}
|
|
779
|
+
function splitTokensOnBreaks(tokens) {
|
|
780
|
+
const segments = [];
|
|
781
|
+
let current = [];
|
|
782
|
+
for (const token of tokens) {
|
|
783
|
+
if (COMPACTION_BREAK_TOKENS.has(token)) {
|
|
784
|
+
if (current.length > 0) {
|
|
785
|
+
segments.push(current);
|
|
786
|
+
current = [];
|
|
787
|
+
}
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
current.push(token);
|
|
791
|
+
}
|
|
792
|
+
if (current.length > 0) {
|
|
793
|
+
segments.push(current);
|
|
794
|
+
}
|
|
795
|
+
return segments;
|
|
796
|
+
}
|
|
797
|
+
function trimWeakLeadingTokens(tokens) {
|
|
798
|
+
let start = 0;
|
|
799
|
+
while (start < tokens.length && COMPACTION_WEAK_LEADING_TOKENS.has(tokens[start] ?? "")) {
|
|
800
|
+
start += 1;
|
|
801
|
+
}
|
|
802
|
+
return tokens.slice(start);
|
|
803
|
+
}
|
|
804
|
+
function joinCompactionReasons(reasons) {
|
|
805
|
+
if (reasons.length <= 1) {
|
|
806
|
+
return reasons[0] ?? "";
|
|
807
|
+
}
|
|
808
|
+
return `${reasons.slice(0, -1).join(", ")} and ${reasons[reasons.length - 1]}`;
|
|
809
|
+
}
|
|
810
|
+
function isRequirementRelation(token) {
|
|
811
|
+
return token === "depend" || token === "depends" || token === "need" || token === "needs" || token === "required" || token === "require" || token === "requires";
|
|
812
|
+
}
|
|
813
|
+
function isOrderingRelation(token) {
|
|
814
|
+
return token === "after" || token === "before" || token === "follow" || token === "follows" || token === "precede" || token === "precedes";
|
|
815
|
+
}
|
|
816
|
+
function isPreservationRelation(token) {
|
|
817
|
+
return token === "keep" || token === "keeps" || token === "maintain" || token === "maintains" || token === "preserve" || token === "preserves" || token === "retain" || token === "retains";
|
|
818
|
+
}
|
|
819
|
+
function startsWithTokens(tokens, prefix) {
|
|
820
|
+
return prefix.every((token, index) => tokens[index] === token);
|
|
821
|
+
}
|
|
822
|
+
function endsWithTokens(tokens, suffix) {
|
|
823
|
+
return suffix.every((token, index) => tokens[tokens.length - suffix.length + index] === token);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/core/claim-slot-policy.ts
|
|
827
|
+
var MULTIVALUED_ATTRIBUTE_HEADS = /* @__PURE__ */ new Set(["access", "dependency", "guide", "integration", "preference", "requirement", "support"]);
|
|
828
|
+
function resolveClaimSlotPolicy(claimKey, config) {
|
|
829
|
+
const normalized = normalizeClaimKey(claimKey ?? "");
|
|
830
|
+
if (!normalized.ok) {
|
|
831
|
+
return {
|
|
832
|
+
policy: "exclusive",
|
|
833
|
+
reason: "No canonical claim key was available, so the slot policy defaulted to exclusive."
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const { claimKey: canonicalClaimKey, entity, attribute } = normalized.value;
|
|
837
|
+
const attributeHead = attribute.split("_")[0] ?? attribute;
|
|
838
|
+
const configuredPolicy = resolveConfiguredAttributeHeadPolicy(attributeHead, config);
|
|
839
|
+
if (configuredPolicy) {
|
|
840
|
+
return {
|
|
841
|
+
claimKey: canonicalClaimKey,
|
|
842
|
+
entity,
|
|
843
|
+
attribute,
|
|
844
|
+
attributeHead,
|
|
845
|
+
policy: configuredPolicy,
|
|
846
|
+
reason: `Attribute head "${attributeHead}" is configured as ${configuredPolicy} by runtime policy.`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (MULTIVALUED_ATTRIBUTE_HEADS.has(attributeHead)) {
|
|
850
|
+
return {
|
|
851
|
+
claimKey: canonicalClaimKey,
|
|
852
|
+
entity,
|
|
853
|
+
attribute,
|
|
854
|
+
attributeHead,
|
|
855
|
+
policy: "multivalued",
|
|
856
|
+
reason: `Attribute head "${attributeHead}" is registered as multivalued.`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
claimKey: canonicalClaimKey,
|
|
861
|
+
entity,
|
|
862
|
+
attribute,
|
|
863
|
+
attributeHead,
|
|
864
|
+
policy: "exclusive",
|
|
865
|
+
reason: `Attribute head "${attributeHead}" defaults to exclusive current-state shaping.`
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function resolveConfiguredAttributeHeadPolicy(attributeHead, config) {
|
|
869
|
+
const configuredPolicy = config?.attributeHeads?.[attributeHead];
|
|
870
|
+
if (configuredPolicy) {
|
|
871
|
+
return configuredPolicy;
|
|
872
|
+
}
|
|
873
|
+
const loweredAttributeHead = attributeHead.toLowerCase();
|
|
874
|
+
if (loweredAttributeHead === attributeHead) {
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
return config?.attributeHeads?.[loweredAttributeHead];
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/core/recall/trace.ts
|
|
881
|
+
var NOOP_RECALL_TRACE_SINK = {
|
|
882
|
+
reportSummary() {
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
function createNoopRecallTraceSink() {
|
|
886
|
+
return NOOP_RECALL_TRACE_SINK;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/core/recall/search.ts
|
|
890
|
+
var MIN_VECTOR_ONLY_EVIDENCE = 0.3;
|
|
891
|
+
var HISTORICAL_STATE_FLAT_RECENCY = 0.5;
|
|
892
|
+
var HISTORICAL_PREDECESSOR_BOOST = 0.08;
|
|
893
|
+
var HISTORICAL_RETIRED_PREDECESSOR_BOOST = 0.06;
|
|
894
|
+
var HISTORICAL_OLDER_STATE_BOOST = 0.08;
|
|
895
|
+
var HISTORICAL_TOPIC_SHARED_PREFIX_MIN = 2;
|
|
896
|
+
var HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN = 0.6;
|
|
897
|
+
var CLAIM_KEY_TENTATIVE_CURRENT_PENALTY = 0.08;
|
|
898
|
+
var CLAIM_KEY_REDUNDANT_TRUSTED_SLOT_PENALTY = 0.05;
|
|
899
|
+
var CLAIM_KEY_REDUNDANT_TRUSTED_SLOT_MAX_PENALTY = 0.15;
|
|
900
|
+
var QUERY_EMBEDDING_FAILURE_NOTICE = "Embeddings failed during recall, so Agenr fell back to lexical-only entry ranking.";
|
|
901
|
+
var VECTOR_SEARCH_FAILURE_NOTICE = "Vector search failed during recall, so Agenr continued with lexical entry candidates only.";
|
|
902
|
+
async function recall(query, ports, options = {}) {
|
|
903
|
+
const text = query.text.trim();
|
|
904
|
+
const limit = normalizeLimit(query.limit);
|
|
905
|
+
const threshold = normalizeThreshold(query.threshold);
|
|
906
|
+
const budget = normalizeBudget(query.budget);
|
|
907
|
+
const asOfDate = query.asOf ? parseAroundDate(query.asOf) : null;
|
|
908
|
+
const aroundDate = query.around !== void 0 ? parseAroundDate(query.around) : inferAroundDate(text);
|
|
909
|
+
const since = query.since ? parseRelativeDate(query.since) : null;
|
|
910
|
+
const until = query.until ? parseRelativeDate(query.until) : null;
|
|
911
|
+
const filters = buildEntryFilters(query.types, query.tags, since, until);
|
|
912
|
+
const trace = options.trace ?? createNoopRecallTraceSink();
|
|
913
|
+
const slotPolicyConfig = options.slotPolicyConfig;
|
|
914
|
+
const summary = buildRecallTraceSummary({
|
|
915
|
+
filters,
|
|
916
|
+
limit,
|
|
917
|
+
threshold,
|
|
918
|
+
budget,
|
|
919
|
+
asOfDate,
|
|
920
|
+
aroundDate,
|
|
921
|
+
aroundSource: query.around !== void 0 ? "explicit" : "inferred",
|
|
922
|
+
aroundRadius: aroundDate ? normalizeAroundRadius(query.aroundRadius) : void 0
|
|
923
|
+
});
|
|
924
|
+
let traceReported = false;
|
|
925
|
+
const reportTrace = (noResultReason) => {
|
|
926
|
+
if (traceReported) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
traceReported = true;
|
|
930
|
+
finishRecallTrace(summary, trace, noResultReason);
|
|
931
|
+
};
|
|
932
|
+
if (text.length === 0) {
|
|
933
|
+
reportTrace("empty_query");
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
if (limit === 0) {
|
|
937
|
+
reportTrace("limit_zero");
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
let queryEmbedding = [];
|
|
942
|
+
try {
|
|
943
|
+
queryEmbedding = await ports.embed(text);
|
|
944
|
+
} catch {
|
|
945
|
+
markRecallDegraded(summary, "query_embedding_failed", QUERY_EMBEDDING_FAILURE_NOTICE);
|
|
946
|
+
}
|
|
947
|
+
const vectorSearchLimit = limit * 4;
|
|
948
|
+
const lexicalSearchLimit = limit * 2;
|
|
949
|
+
const [vectorCandidates, ftsCandidates] = queryEmbedding.length > 0 ? await Promise.all([
|
|
950
|
+
ports.vectorSearch({
|
|
951
|
+
embedding: queryEmbedding,
|
|
952
|
+
limit: vectorSearchLimit,
|
|
953
|
+
filters
|
|
954
|
+
}).catch(() => {
|
|
955
|
+
markRecallDegraded(summary, "vector_search_failed", VECTOR_SEARCH_FAILURE_NOTICE);
|
|
956
|
+
return [];
|
|
957
|
+
}),
|
|
958
|
+
ports.ftsSearch({
|
|
959
|
+
text,
|
|
960
|
+
limit: lexicalSearchLimit,
|
|
961
|
+
filters
|
|
962
|
+
})
|
|
963
|
+
]) : [
|
|
964
|
+
[],
|
|
965
|
+
await ports.ftsSearch({
|
|
966
|
+
text,
|
|
967
|
+
limit: lexicalSearchLimit,
|
|
968
|
+
filters
|
|
969
|
+
})
|
|
970
|
+
];
|
|
971
|
+
summary.degraded.lexicalOnly = summary.degraded.active && queryEmbedding.length === 0;
|
|
972
|
+
const mergeStartedAt = Date.now();
|
|
973
|
+
const mergedCandidates = mergeCandidates(vectorCandidates, ftsCandidates);
|
|
974
|
+
await expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, {
|
|
975
|
+
activeEntryIds: Array.from(mergedCandidates.keys()),
|
|
976
|
+
rankingProfile: query.rankingProfile
|
|
977
|
+
});
|
|
978
|
+
summary.candidateCounts.merged = mergedCandidates.size;
|
|
979
|
+
summary.timings.mergeCandidatesMs = elapsedMs(mergeStartedAt);
|
|
980
|
+
const scoreStartedAt = Date.now();
|
|
981
|
+
const scored = applyClaimKeyResultShaping(
|
|
982
|
+
applyHistoricalLineageBoosts(
|
|
983
|
+
Array.from(mergedCandidates.values()).map(
|
|
984
|
+
(candidate) => scoreMergedCandidate(candidate, text, queryEmbedding, {
|
|
985
|
+
asOfDate,
|
|
986
|
+
aroundDate,
|
|
987
|
+
aroundRadius: query.aroundRadius,
|
|
988
|
+
rankingProfile: query.rankingProfile
|
|
989
|
+
})
|
|
990
|
+
),
|
|
991
|
+
{
|
|
992
|
+
aroundDate,
|
|
993
|
+
rankingProfile: query.rankingProfile
|
|
994
|
+
},
|
|
995
|
+
summary.claimKey,
|
|
996
|
+
slotPolicyConfig
|
|
997
|
+
),
|
|
998
|
+
summary.claimKey,
|
|
999
|
+
slotPolicyConfig
|
|
1000
|
+
).sort((left, right) => right.score - left.score);
|
|
1001
|
+
summary.timings.scoreCandidatesMs = elapsedMs(scoreStartedAt);
|
|
1002
|
+
const thresholdStartedAt = Date.now();
|
|
1003
|
+
const thresholded = scored.filter((result) => hasSufficientReturnEvidence(result) && result.score >= threshold);
|
|
1004
|
+
summary.candidateCounts.thresholdQualified = thresholded.length;
|
|
1005
|
+
summary.timings.thresholdMs = elapsedMs(thresholdStartedAt);
|
|
1006
|
+
if (thresholded.length === 0) {
|
|
1007
|
+
reportTrace(resolveNoResultReason(summary, scored.length === 0 ? "no_candidates" : "below_threshold"));
|
|
1008
|
+
return [];
|
|
1009
|
+
}
|
|
1010
|
+
const budgetStartedAt = Date.now();
|
|
1011
|
+
const budgeted = budget === null ? thresholded : applyBudget(thresholded, budget);
|
|
1012
|
+
summary.candidateCounts.budgetAccepted = budgeted.length;
|
|
1013
|
+
summary.timings.budgetMs = budget === null ? 0 : elapsedMs(budgetStartedAt);
|
|
1014
|
+
const ranked = budgeted.slice(0, limit);
|
|
1015
|
+
summary.candidateCounts.finalRanked = ranked.length;
|
|
1016
|
+
if (ranked.length === 0) {
|
|
1017
|
+
reportTrace("limit_zero");
|
|
1018
|
+
return [];
|
|
1019
|
+
}
|
|
1020
|
+
const hydratedEntries = await ports.hydrateEntries(ranked.map((result) => result.entry.id));
|
|
1021
|
+
const shapeStartedAt = Date.now();
|
|
1022
|
+
const hydratedById = new Map(hydratedEntries.map((entry) => [entry.id, entry]));
|
|
1023
|
+
const results = ranked.flatMap((result) => {
|
|
1024
|
+
const entry = hydratedById.get(result.entry.id);
|
|
1025
|
+
if (!entry) {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
return [
|
|
1029
|
+
{
|
|
1030
|
+
entry,
|
|
1031
|
+
score: result.score,
|
|
1032
|
+
scores: result.scores
|
|
1033
|
+
}
|
|
1034
|
+
];
|
|
1035
|
+
});
|
|
1036
|
+
summary.candidateCounts.returned = results.length;
|
|
1037
|
+
summary.timings.shapeResultsMs = elapsedMs(shapeStartedAt);
|
|
1038
|
+
if (results.length === 0) {
|
|
1039
|
+
reportTrace(resolveNoResultReason(summary, "hydrate_missing"));
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
if (results.length > 0) {
|
|
1043
|
+
await ports.recordRecallEvents({
|
|
1044
|
+
entryIds: results.map((result) => result.entry.id),
|
|
1045
|
+
query: text,
|
|
1046
|
+
sessionKey: query.sessionKey
|
|
1047
|
+
}).catch(() => void 0);
|
|
1048
|
+
}
|
|
1049
|
+
reportTrace();
|
|
1050
|
+
return results;
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
reportTrace();
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function buildRecallTraceSummary(params) {
|
|
1057
|
+
return {
|
|
1058
|
+
filtering: {
|
|
1059
|
+
types: params.filters?.types ?? [],
|
|
1060
|
+
tags: params.filters?.tags ?? [],
|
|
1061
|
+
since: params.filters?.since?.toISOString(),
|
|
1062
|
+
until: params.filters?.until?.toISOString(),
|
|
1063
|
+
around: params.aroundDate ? {
|
|
1064
|
+
source: params.aroundSource,
|
|
1065
|
+
anchor: params.aroundDate.toISOString(),
|
|
1066
|
+
radiusDays: params.aroundRadius ?? 14
|
|
1067
|
+
} : void 0,
|
|
1068
|
+
...params.asOfDate ? {
|
|
1069
|
+
asOf: {
|
|
1070
|
+
anchor: params.asOfDate.toISOString()
|
|
1071
|
+
}
|
|
1072
|
+
} : {}
|
|
1073
|
+
},
|
|
1074
|
+
ranking: {
|
|
1075
|
+
limit: params.limit,
|
|
1076
|
+
threshold: params.threshold,
|
|
1077
|
+
budget: params.budget
|
|
1078
|
+
},
|
|
1079
|
+
candidateCounts: {
|
|
1080
|
+
merged: 0,
|
|
1081
|
+
thresholdQualified: 0,
|
|
1082
|
+
budgetAccepted: 0,
|
|
1083
|
+
finalRanked: 0,
|
|
1084
|
+
returned: 0
|
|
1085
|
+
},
|
|
1086
|
+
claimKey: {
|
|
1087
|
+
historicalBoosted: 0,
|
|
1088
|
+
tentativeLineageSuppressed: 0,
|
|
1089
|
+
trustPenalized: 0,
|
|
1090
|
+
redundancyPenalized: 0
|
|
1091
|
+
},
|
|
1092
|
+
timings: {
|
|
1093
|
+
mergeCandidatesMs: 0,
|
|
1094
|
+
scoreCandidatesMs: 0,
|
|
1095
|
+
thresholdMs: 0,
|
|
1096
|
+
budgetMs: 0,
|
|
1097
|
+
shapeResultsMs: 0
|
|
1098
|
+
},
|
|
1099
|
+
degraded: {
|
|
1100
|
+
active: false,
|
|
1101
|
+
reasons: [],
|
|
1102
|
+
lexicalOnly: false,
|
|
1103
|
+
notices: []
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function finishRecallTrace(summary, trace, noResultReason) {
|
|
1108
|
+
if (noResultReason) {
|
|
1109
|
+
summary.ranking.noResultReason = noResultReason;
|
|
1110
|
+
}
|
|
1111
|
+
trace.reportSummary(summary);
|
|
1112
|
+
}
|
|
1113
|
+
function markRecallDegraded(summary, reason, notice) {
|
|
1114
|
+
summary.degraded.active = true;
|
|
1115
|
+
if (!summary.degraded.reasons.includes(reason)) {
|
|
1116
|
+
summary.degraded.reasons.push(reason);
|
|
1117
|
+
}
|
|
1118
|
+
if (!summary.degraded.notices.includes(notice)) {
|
|
1119
|
+
summary.degraded.notices.push(notice);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function resolveNoResultReason(summary, reason) {
|
|
1123
|
+
if (!summary.degraded.active) {
|
|
1124
|
+
return reason;
|
|
1125
|
+
}
|
|
1126
|
+
if (reason === "no_candidates") {
|
|
1127
|
+
return "degraded_no_candidates";
|
|
1128
|
+
}
|
|
1129
|
+
if (reason === "below_threshold") {
|
|
1130
|
+
return "degraded_below_threshold";
|
|
1131
|
+
}
|
|
1132
|
+
return reason;
|
|
1133
|
+
}
|
|
1134
|
+
function scoreMergedCandidate(candidate, queryText, queryEmbedding, params) {
|
|
1135
|
+
const vector = candidate.vectorSim ?? cosineSimilarity(candidate.entry.embedding ?? [], queryEmbedding);
|
|
1136
|
+
const lexical = computeLexicalScore(queryText, candidate.entry.subject, candidate.entry.content);
|
|
1137
|
+
const recency = resolveRecencyScore(candidate.entry, params);
|
|
1138
|
+
const importance = importanceScore(candidate.entry.importance);
|
|
1139
|
+
const scored = scoreCandidate({
|
|
1140
|
+
vectorSim: vector,
|
|
1141
|
+
lexical,
|
|
1142
|
+
recency,
|
|
1143
|
+
importance
|
|
1144
|
+
});
|
|
1145
|
+
return {
|
|
1146
|
+
entry: candidate.entry,
|
|
1147
|
+
score: scored.score,
|
|
1148
|
+
scores: {
|
|
1149
|
+
...scored.scores,
|
|
1150
|
+
historicalLineage: 0,
|
|
1151
|
+
claimKeyTrustPenalty: 0,
|
|
1152
|
+
claimKeyRedundancyPenalty: 0
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
async function expandHistoricalCandidates(mergedCandidates, queryEmbedding, ports, params) {
|
|
1157
|
+
if (params.rankingProfile !== "historical_state" || mergedCandidates.size === 0 || !ports.fetchPredecessors) {
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const predecessors = await ports.fetchPredecessors({
|
|
1161
|
+
activeEntryIds: params.activeEntryIds
|
|
1162
|
+
});
|
|
1163
|
+
for (const entry of predecessors) {
|
|
1164
|
+
if (mergedCandidates.has(entry.id)) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
mergedCandidates.set(entry.id, {
|
|
1168
|
+
entry,
|
|
1169
|
+
vectorSim: cosineSimilarity(entry.embedding ?? [], queryEmbedding)
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function resolveRecencyScore(entry, params) {
|
|
1174
|
+
if (params.asOfDate) {
|
|
1175
|
+
return resolveAsOfScore(entry, params.asOfDate);
|
|
1176
|
+
}
|
|
1177
|
+
if (params.aroundDate) {
|
|
1178
|
+
return gaussianRecency(entry.created_at, params.aroundDate, normalizeAroundRadius(params.aroundRadius));
|
|
1179
|
+
}
|
|
1180
|
+
if (params.rankingProfile === "historical_state") {
|
|
1181
|
+
return HISTORICAL_STATE_FLAT_RECENCY;
|
|
1182
|
+
}
|
|
1183
|
+
return recencyScore(entry.created_at, entry.expiry);
|
|
1184
|
+
}
|
|
1185
|
+
function resolveAsOfScore(entry, asOfDate) {
|
|
1186
|
+
const validFrom = parseTimestamp(entry.valid_from);
|
|
1187
|
+
const validTo = parseTimestamp(entry.valid_to);
|
|
1188
|
+
if (validFrom || validTo) {
|
|
1189
|
+
const startMs = validFrom?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
1190
|
+
const endMs = validTo?.getTime() ?? Number.POSITIVE_INFINITY;
|
|
1191
|
+
const asOfMs = asOfDate.getTime();
|
|
1192
|
+
if (asOfMs >= startMs && asOfMs <= endMs) {
|
|
1193
|
+
return 1;
|
|
1194
|
+
}
|
|
1195
|
+
const nearestBoundaryMs = asOfMs < startMs ? startMs : endMs;
|
|
1196
|
+
return Math.max(0.1, gaussianRecency(new Date(nearestBoundaryMs).toISOString(), asOfDate, 21) * 0.65);
|
|
1197
|
+
}
|
|
1198
|
+
const observedAt = parseTimestamp(entry.claim_support_observed_at);
|
|
1199
|
+
if (observedAt) {
|
|
1200
|
+
const observedBeforeAsOf = observedAt.getTime() <= asOfDate.getTime();
|
|
1201
|
+
const proximity = gaussianRecency(observedAt.toISOString(), asOfDate, 30);
|
|
1202
|
+
return observedBeforeAsOf ? Math.max(0.45, proximity * 0.8) : Math.max(0.05, proximity * 0.2);
|
|
1203
|
+
}
|
|
1204
|
+
const createdAt = parseTimestamp(entry.created_at);
|
|
1205
|
+
if (createdAt) {
|
|
1206
|
+
const createdBeforeAsOf = createdAt.getTime() <= asOfDate.getTime();
|
|
1207
|
+
const proximity = gaussianRecency(createdAt.toISOString(), asOfDate, 45);
|
|
1208
|
+
return createdBeforeAsOf ? Math.max(0.35, proximity * 0.7) : Math.max(0.05, proximity * 0.15);
|
|
1209
|
+
}
|
|
1210
|
+
return HISTORICAL_STATE_FLAT_RECENCY;
|
|
1211
|
+
}
|
|
1212
|
+
function applyHistoricalLineageBoosts(candidates, params, claimKeyTrace, slotPolicyConfig) {
|
|
1213
|
+
if (params.rankingProfile !== "historical_state") {
|
|
1214
|
+
return candidates;
|
|
1215
|
+
}
|
|
1216
|
+
const entries = candidates.map((candidate) => candidate.entry);
|
|
1217
|
+
return candidates.map((candidate) => {
|
|
1218
|
+
const decision = resolveHistoricalLineageBonus(candidate.entry, entries, params.aroundDate, slotPolicyConfig);
|
|
1219
|
+
if (decision.tentativeLineageSuppressed) {
|
|
1220
|
+
claimKeyTrace.tentativeLineageSuppressed += 1;
|
|
1221
|
+
}
|
|
1222
|
+
const bonus = decision.bonus;
|
|
1223
|
+
if (bonus <= 0) {
|
|
1224
|
+
return candidate;
|
|
1225
|
+
}
|
|
1226
|
+
claimKeyTrace.historicalBoosted += 1;
|
|
1227
|
+
return {
|
|
1228
|
+
...candidate,
|
|
1229
|
+
score: clampRecallScore(candidate.score + bonus),
|
|
1230
|
+
scores: {
|
|
1231
|
+
...candidate.scores,
|
|
1232
|
+
historicalLineage: candidate.scores.historicalLineage + bonus
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
function resolveHistoricalLineageBonus(entry, entries, aroundDate, slotPolicyConfig) {
|
|
1238
|
+
if (entries.some((peer) => peer.id !== entry.id && entry.superseded_by === peer.id)) {
|
|
1239
|
+
return {
|
|
1240
|
+
bonus: HISTORICAL_PREDECESSOR_BOOST,
|
|
1241
|
+
tentativeLineageSuppressed: false
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
if (aroundDate) {
|
|
1245
|
+
return {
|
|
1246
|
+
bonus: 0,
|
|
1247
|
+
tentativeLineageSuppressed: false
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
let tentativeLineageSuppressed = false;
|
|
1251
|
+
for (const peer of entries) {
|
|
1252
|
+
if (peer.id === entry.id || !isPotentialCurrentPeer(peer) || createdAtMs(entry.created_at) >= createdAtMs(peer.created_at)) {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const relation = resolveHistoricalPeerRelation(entry, peer, entries, slotPolicyConfig);
|
|
1256
|
+
if (relation === "tentative_claim_key_suppressed") {
|
|
1257
|
+
tentativeLineageSuppressed = true;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (relation === null) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
bonus: entry.retired ? HISTORICAL_RETIRED_PREDECESSOR_BOOST : HISTORICAL_OLDER_STATE_BOOST,
|
|
1265
|
+
tentativeLineageSuppressed
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
bonus: 0,
|
|
1270
|
+
tentativeLineageSuppressed
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function isPotentialCurrentPeer(entry) {
|
|
1274
|
+
return !entry.retired && entry.superseded_by === void 0;
|
|
1275
|
+
}
|
|
1276
|
+
function resolveHistoricalPeerRelation(left, right, entries, slotPolicyConfig) {
|
|
1277
|
+
if (left.claim_key && right.claim_key && left.claim_key === right.claim_key) {
|
|
1278
|
+
if (resolveClaimSlotPolicy(left.claim_key, slotPolicyConfig).policy === "multivalued") {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
return canUseClaimKeyLineage(left, entries, slotPolicyConfig) ? "claim_key" : "tentative_claim_key_suppressed";
|
|
1282
|
+
}
|
|
1283
|
+
return sharesHistoricalTopic(left, right) ? "topic" : null;
|
|
1284
|
+
}
|
|
1285
|
+
function canUseClaimKeyLineage(entry, entries, slotPolicyConfig) {
|
|
1286
|
+
if (!entry.claim_key) {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
if (resolveClaimSlotPolicy(entry.claim_key, slotPolicyConfig).policy === "multivalued") {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
if (!hasTrustedClaimKeyEvidence(entries, entry.claim_key)) {
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
return entry.claim_key_status === "trusted";
|
|
1296
|
+
}
|
|
1297
|
+
function hasTrustedClaimKeyEvidence(entries, claimKey) {
|
|
1298
|
+
return entries.some((entry) => entry.claim_key === claimKey && entry.claim_key_status === "trusted");
|
|
1299
|
+
}
|
|
1300
|
+
function sharesHistoricalTopic(left, right) {
|
|
1301
|
+
const leftTokens = tokenize(left.subject);
|
|
1302
|
+
const rightTokens = tokenize(right.subject);
|
|
1303
|
+
if (leftTokens.length === 0 || rightTokens.length === 0) {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
const sharedPrefixCount = countSharedPrefixTokens(leftTokens, rightTokens);
|
|
1307
|
+
return sharedPrefixCount >= HISTORICAL_TOPIC_SHARED_PREFIX_MIN && sharedPrefixCount / leftTokens.length >= HISTORICAL_TOPIC_PREFIX_OF_CANDIDATE_MIN;
|
|
1308
|
+
}
|
|
1309
|
+
function createdAtMs(value) {
|
|
1310
|
+
return parseTimestamp(value)?.getTime() ?? 0;
|
|
1311
|
+
}
|
|
1312
|
+
function parseTimestamp(value) {
|
|
1313
|
+
const normalized = value?.trim();
|
|
1314
|
+
if (!normalized) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
const timestamp = new Date(normalized);
|
|
1318
|
+
return Number.isFinite(timestamp.getTime()) ? timestamp : null;
|
|
1319
|
+
}
|
|
1320
|
+
function countSharedPrefixTokens(leftTokens, rightTokens) {
|
|
1321
|
+
const length = Math.min(leftTokens.length, rightTokens.length);
|
|
1322
|
+
let sharedPrefixCount = 0;
|
|
1323
|
+
for (let index = 0; index < length; index += 1) {
|
|
1324
|
+
if (leftTokens[index] !== rightTokens[index]) {
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
sharedPrefixCount += 1;
|
|
1328
|
+
}
|
|
1329
|
+
return sharedPrefixCount;
|
|
1330
|
+
}
|
|
1331
|
+
function applyClaimKeyResultShaping(candidates, claimKeyTrace, slotPolicyConfig) {
|
|
1332
|
+
if (candidates.length === 0) {
|
|
1333
|
+
return candidates;
|
|
1334
|
+
}
|
|
1335
|
+
const trustedActiveClaimKeys = new Set(
|
|
1336
|
+
candidates.map((candidate) => candidate.entry).filter(
|
|
1337
|
+
(entry) => isPotentialCurrentPeer(entry) && entry.claim_key && entry.claim_key_status === "trusted" && resolveClaimSlotPolicy(entry.claim_key, slotPolicyConfig).policy === "exclusive"
|
|
1338
|
+
).map((entry) => entry.claim_key)
|
|
1339
|
+
);
|
|
1340
|
+
const trustedSlotRankById = rankTrustedSlotSiblings(candidates, slotPolicyConfig);
|
|
1341
|
+
return candidates.map((candidate) => {
|
|
1342
|
+
const trustPenalty = shouldPenalizeTentativeCurrentSibling(candidate.entry, trustedActiveClaimKeys) ? CLAIM_KEY_TENTATIVE_CURRENT_PENALTY : 0;
|
|
1343
|
+
const redundancyPenalty = resolveTrustedSlotRedundancyPenalty(candidate.entry.id, trustedSlotRankById);
|
|
1344
|
+
if (trustPenalty <= 0 && redundancyPenalty <= 0) {
|
|
1345
|
+
return candidate;
|
|
1346
|
+
}
|
|
1347
|
+
if (trustPenalty > 0) {
|
|
1348
|
+
claimKeyTrace.trustPenalized += 1;
|
|
1349
|
+
}
|
|
1350
|
+
if (redundancyPenalty > 0) {
|
|
1351
|
+
claimKeyTrace.redundancyPenalized += 1;
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
...candidate,
|
|
1355
|
+
score: clampRecallScore(candidate.score - trustPenalty - redundancyPenalty),
|
|
1356
|
+
scores: {
|
|
1357
|
+
...candidate.scores,
|
|
1358
|
+
claimKeyTrustPenalty: trustPenalty,
|
|
1359
|
+
claimKeyRedundancyPenalty: redundancyPenalty
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function rankTrustedSlotSiblings(candidates, slotPolicyConfig) {
|
|
1365
|
+
const candidatesById = new Map(candidates.map((candidate) => [candidate.entry.id, candidate]));
|
|
1366
|
+
const trustedByClaimKey = /* @__PURE__ */ new Map();
|
|
1367
|
+
for (const candidate of candidates) {
|
|
1368
|
+
const claimKey = candidate.entry.claim_key;
|
|
1369
|
+
if (!claimKey || candidate.entry.claim_key_status !== "trusted" || !isPotentialCurrentPeer(candidate.entry) || resolveClaimSlotPolicy(claimKey, slotPolicyConfig).policy !== "exclusive") {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const siblings = trustedByClaimKey.get(claimKey) ?? [];
|
|
1373
|
+
siblings.push(candidate);
|
|
1374
|
+
trustedByClaimKey.set(claimKey, siblings);
|
|
1375
|
+
}
|
|
1376
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const siblings of trustedByClaimKey.values()) {
|
|
1378
|
+
siblings.slice().sort(compareCandidatesForTrustedSlotRank).forEach((candidate, index) => {
|
|
1379
|
+
if (candidatesById.has(candidate.entry.id)) {
|
|
1380
|
+
ranks.set(candidate.entry.id, index);
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
return ranks;
|
|
1385
|
+
}
|
|
1386
|
+
function compareCandidatesForTrustedSlotRank(left, right) {
|
|
1387
|
+
return right.score - left.score || createdAtMs(right.entry.created_at) - createdAtMs(left.entry.created_at) || left.entry.id.localeCompare(right.entry.id);
|
|
1388
|
+
}
|
|
1389
|
+
function shouldPenalizeTentativeCurrentSibling(entry, trustedActiveClaimKeys) {
|
|
1390
|
+
return isPotentialCurrentPeer(entry) && entry.claim_key !== void 0 && entry.claim_key_status !== "trusted" && trustedActiveClaimKeys.has(entry.claim_key);
|
|
1391
|
+
}
|
|
1392
|
+
function resolveTrustedSlotRedundancyPenalty(entryId, trustedSlotRankById) {
|
|
1393
|
+
const rank = trustedSlotRankById.get(entryId) ?? 0;
|
|
1394
|
+
if (rank <= 0) {
|
|
1395
|
+
return 0;
|
|
1396
|
+
}
|
|
1397
|
+
return Math.min(CLAIM_KEY_REDUNDANT_TRUSTED_SLOT_MAX_PENALTY, rank * CLAIM_KEY_REDUNDANT_TRUSTED_SLOT_PENALTY);
|
|
1398
|
+
}
|
|
1399
|
+
function clampRecallScore(value) {
|
|
1400
|
+
return Math.max(0, Math.min(1, value));
|
|
1401
|
+
}
|
|
1402
|
+
function hasSufficientReturnEvidence(candidate) {
|
|
1403
|
+
if (candidate.scores.lexical > 0) {
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
return candidate.scores.vector >= MIN_VECTOR_ONLY_EVIDENCE;
|
|
1407
|
+
}
|
|
1408
|
+
function mergeCandidates(vectorCandidates, ftsCandidates) {
|
|
1409
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1410
|
+
for (const candidate of vectorCandidates) {
|
|
1411
|
+
merged.set(candidate.entry.id, {
|
|
1412
|
+
entry: candidate.entry,
|
|
1413
|
+
vectorSim: candidate.vectorSim
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
for (const candidate of ftsCandidates) {
|
|
1417
|
+
const existing = merged.get(candidate.entry.id);
|
|
1418
|
+
if (existing) {
|
|
1419
|
+
existing.entry = existing.entry.embedding ? existing.entry : candidate.entry;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
merged.set(candidate.entry.id, {
|
|
1423
|
+
entry: candidate.entry
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return merged;
|
|
1427
|
+
}
|
|
1428
|
+
function buildEntryFilters(types, tags, since, until) {
|
|
1429
|
+
const filters = {};
|
|
1430
|
+
if (types && types.length > 0) {
|
|
1431
|
+
filters.types = types;
|
|
1432
|
+
}
|
|
1433
|
+
if (tags && tags.length > 0) {
|
|
1434
|
+
filters.tags = tags;
|
|
1435
|
+
}
|
|
1436
|
+
if (since) {
|
|
1437
|
+
filters.since = since;
|
|
1438
|
+
}
|
|
1439
|
+
if (until) {
|
|
1440
|
+
filters.until = until;
|
|
1441
|
+
}
|
|
1442
|
+
return Object.keys(filters).length > 0 ? filters : void 0;
|
|
1443
|
+
}
|
|
1444
|
+
function applyBudget(results, budget) {
|
|
1445
|
+
if (results.length === 0) {
|
|
1446
|
+
return [];
|
|
1447
|
+
}
|
|
1448
|
+
const accepted = [results[0]];
|
|
1449
|
+
let consumed = estimateTokens(results[0].entry);
|
|
1450
|
+
for (const result of results.slice(1)) {
|
|
1451
|
+
const estimate = estimateTokens(result.entry);
|
|
1452
|
+
if (consumed + estimate > budget) {
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
accepted.push(result);
|
|
1456
|
+
consumed += estimate;
|
|
1457
|
+
}
|
|
1458
|
+
return accepted;
|
|
1459
|
+
}
|
|
1460
|
+
function estimateTokens(entry) {
|
|
1461
|
+
return (entry.subject.length + entry.content.length) / 4;
|
|
1462
|
+
}
|
|
1463
|
+
function parseAroundDate(value) {
|
|
1464
|
+
return parseRelativeDate(value) ?? inferAroundDate(value);
|
|
1465
|
+
}
|
|
1466
|
+
function normalizeLimit(value) {
|
|
1467
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1468
|
+
return 10;
|
|
1469
|
+
}
|
|
1470
|
+
return Math.max(0, Math.floor(value));
|
|
1471
|
+
}
|
|
1472
|
+
function normalizeThreshold(value) {
|
|
1473
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1474
|
+
return 0;
|
|
1475
|
+
}
|
|
1476
|
+
return Math.min(1, Math.max(0, value));
|
|
1477
|
+
}
|
|
1478
|
+
function normalizeBudget(value) {
|
|
1479
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
return Math.max(0, value);
|
|
1483
|
+
}
|
|
1484
|
+
function normalizeAroundRadius(value) {
|
|
1485
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
1486
|
+
return 14;
|
|
1487
|
+
}
|
|
1488
|
+
return value;
|
|
1489
|
+
}
|
|
1490
|
+
function elapsedMs(startedAt) {
|
|
1491
|
+
return Math.max(0, Date.now() - startedAt);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
export {
|
|
1495
|
+
recencyScore,
|
|
1496
|
+
gaussianRecency,
|
|
1497
|
+
importanceScore,
|
|
1498
|
+
combinedRelevance,
|
|
1499
|
+
scoreCandidate,
|
|
1500
|
+
cosineSimilarity,
|
|
1501
|
+
normalizeClaimKeySegment,
|
|
1502
|
+
normalizeClaimKey,
|
|
1503
|
+
compactClaimKey,
|
|
1504
|
+
validateExtractedClaimKey,
|
|
1505
|
+
inspectClaimKey,
|
|
1506
|
+
isTrustedClaimKeyForCleanup,
|
|
1507
|
+
describeClaimKeyNormalizationFailure,
|
|
1508
|
+
describeExtractedClaimKeyRejection,
|
|
1509
|
+
describeClaimKeySuspicion,
|
|
1510
|
+
inferAroundDate,
|
|
1511
|
+
parseRelativeDate,
|
|
1512
|
+
tokenize,
|
|
1513
|
+
buildLexicalPlan,
|
|
1514
|
+
computeLexicalScore,
|
|
1515
|
+
resolveClaimSlotPolicy,
|
|
1516
|
+
recall
|
|
1517
|
+
};
|