@lorekit/cli 1.40.0 → 1.41.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/package.json +1 -1
- package/src/lessons-pure.mjs +44 -6
package/package.json
CHANGED
package/src/lessons-pure.mjs
CHANGED
|
@@ -212,7 +212,7 @@ export function resolveScopeKeyArgs(positionals = [], options = {}) {
|
|
|
212
212
|
// takes every slot, evicting the durable lessons that have been re-learned a
|
|
213
213
|
// dozen times. Recency is a signal, not the ranking.
|
|
214
214
|
//
|
|
215
|
-
// The score is a weighted sum of
|
|
215
|
+
// The score is a weighted sum of four factors, each normalised to [0,1]:
|
|
216
216
|
//
|
|
217
217
|
// recency — exponential decay on age. Half-life, not a cliff: a lesson does
|
|
218
218
|
// not stop mattering on a particular day.
|
|
@@ -225,6 +225,12 @@ export function resolveScopeKeyArgs(positionals = [], options = {}) {
|
|
|
225
225
|
// when no terms are supplied, which is the SessionStart case: it
|
|
226
226
|
// then contributes the same constant to every candidate and the
|
|
227
227
|
// ordering is recency + salience alone.
|
|
228
|
+
// outcome — applied/resolution history in [0,1]. The factor only ever
|
|
229
|
+
// LIFTS: a lesson tagged on an outcome bus scores 1.0 and one
|
|
230
|
+
// carried to a PR 0.75, while a lesson with no history gets the
|
|
231
|
+
// COLD_START_OUTCOME_PRIOR (0.5) — the neutral floor, never 0.
|
|
232
|
+
// So a proven lesson ranks up; an unproven one is not penalised
|
|
233
|
+
// for lacking history and rides on recency and relevance.
|
|
228
234
|
//
|
|
229
235
|
// PURE AND TOTAL, with one scoped exception. `now` is a PARAMETER: the
|
|
230
236
|
// arithmetic never reads the clock, every factor is a function of the value
|
|
@@ -243,7 +249,7 @@ export function resolveScopeKeyArgs(positionals = [], options = {}) {
|
|
|
243
249
|
// nothing — a year-old lesson that has recurred 30 times still deserves a slot.
|
|
244
250
|
export const RECENCY_HALF_LIFE_DAYS = 14;
|
|
245
251
|
|
|
246
|
-
// Equal
|
|
252
|
+
// Equal quarters. Deliberately not tuned: with no corpus to tune against, an
|
|
247
253
|
// invented weighting is a guess wearing a decimal point. They are a parameter
|
|
248
254
|
// so a caller can experiment, and so a future PR can change them with evidence.
|
|
249
255
|
//
|
|
@@ -253,7 +259,21 @@ export const RECENCY_HALF_LIFE_DAYS = 14;
|
|
|
253
259
|
// recursion (a real `RangeError`, raised inside a hook the header promises will
|
|
254
260
|
// never throw). Freezing makes the corruption a `TypeError` at the assignment,
|
|
255
261
|
// in the caller's own frame, instead of a stack overflow three layers down.
|
|
256
|
-
export const DEFAULT_RANK_WEIGHTS = Object.freeze({ recency: 1, salience: 1, relevance: 1 });
|
|
262
|
+
export const DEFAULT_RANK_WEIGHTS = Object.freeze({ recency: 1, salience: 1, relevance: 1, outcome: 1 });
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The cold-start prior for the outcome factor. A new lesson with no applied /
|
|
266
|
+
* resolution history gets this value rather than 0. The rationale: scoring
|
|
267
|
+
* absent outcome at 0 would sink every new lesson below stale ones purely for
|
|
268
|
+
* lacking outcome history (outcome-lag). 0.5 is the neutral midpoint of [0,1]
|
|
269
|
+
* — a cold lesson contributes an average outcome term, so it ranks on
|
|
270
|
+
* recency and relevance instead of being penalised for being new.
|
|
271
|
+
*
|
|
272
|
+
* This is the ONE deliberate asymmetry vs `normalizeRelevance` (which returns
|
|
273
|
+
* 0 for absent / unreadable input). Mirrored byte-identically in
|
|
274
|
+
* `packages/mcp-core/src/lesson-rank.ts` and its edge twin.
|
|
275
|
+
*/
|
|
276
|
+
export const COLD_START_OUTCOME_PRIOR = 0.5;
|
|
257
277
|
|
|
258
278
|
// Two scores closer than this are the same score. Sized well below any
|
|
259
279
|
// difference the factors can produce meaningfully (a one-second age gap moves a
|
|
@@ -379,6 +399,21 @@ function distinctTerms(terms) {
|
|
|
379
399
|
);
|
|
380
400
|
}
|
|
381
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Normalize an outcome value into [0,1]. Absent or unreadable input returns
|
|
404
|
+
* `COLD_START_OUTCOME_PRIOR` — the deliberate asymmetry vs `normalizeRelevance`
|
|
405
|
+
* (which returns 0 for absent input). A present value is clamped to [0,1].
|
|
406
|
+
*
|
|
407
|
+
* The cold-start prior ensures a new lesson with no outcome history is not
|
|
408
|
+
* penalised during outcome-lag — it contributes an average outcome term and
|
|
409
|
+
* ranks on recency + relevance instead.
|
|
410
|
+
*/
|
|
411
|
+
export function normalizeOutcome(value) {
|
|
412
|
+
const n = typeof value === 'string' ? Number(value) : value;
|
|
413
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) return COLD_START_OUTCOME_PRIOR;
|
|
414
|
+
return Math.min(1, Math.max(0, n));
|
|
415
|
+
}
|
|
416
|
+
|
|
382
417
|
/**
|
|
383
418
|
* Score one lesson in [0,1].
|
|
384
419
|
*
|
|
@@ -415,21 +450,24 @@ function scoreWithTerms(entry, termSet, { now, weights, maxSeenCount, halfLifeDa
|
|
|
415
450
|
recency: numberOr(weights?.recency, DEFAULT_RANK_WEIGHTS.recency),
|
|
416
451
|
salience: numberOr(weights?.salience, DEFAULT_RANK_WEIGHTS.salience),
|
|
417
452
|
relevance: numberOr(weights?.relevance, DEFAULT_RANK_WEIGHTS.relevance),
|
|
453
|
+
outcome: numberOr(weights?.outcome, DEFAULT_RANK_WEIGHTS.outcome),
|
|
418
454
|
};
|
|
419
|
-
let total = w.recency + w.salience + w.relevance;
|
|
455
|
+
let total = w.recency + w.salience + w.relevance + w.outcome;
|
|
420
456
|
if (!(total > 0)) {
|
|
421
457
|
w = {
|
|
422
458
|
recency: numberOr(DEFAULT_RANK_WEIGHTS.recency, 0),
|
|
423
459
|
salience: numberOr(DEFAULT_RANK_WEIGHTS.salience, 0),
|
|
424
460
|
relevance: numberOr(DEFAULT_RANK_WEIGHTS.relevance, 0),
|
|
461
|
+
outcome: numberOr(DEFAULT_RANK_WEIGHTS.outcome, 0),
|
|
425
462
|
};
|
|
426
|
-
total = w.recency + w.salience + w.relevance;
|
|
463
|
+
total = w.recency + w.salience + w.relevance + w.outcome;
|
|
427
464
|
}
|
|
428
465
|
if (!(total > 0)) return 0;
|
|
429
466
|
const recency = recencyFactor(entry?.updatedAt ?? entry?.updated_at ?? entry?.updated, now, halfLifeDays);
|
|
430
467
|
const salience = salienceFactor(seenCountFrom(entry), maxSeenCount);
|
|
431
468
|
const relevance = relevanceFromTerms(entry, termSet);
|
|
432
|
-
|
|
469
|
+
const outcome = normalizeOutcome(entry?.outcome);
|
|
470
|
+
return (w.recency * recency + w.salience * salience + w.relevance * relevance + w.outcome * outcome) / total;
|
|
433
471
|
}
|
|
434
472
|
|
|
435
473
|
// A non-negative finite number, or the fallback. Guards a caller passing a
|