@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorekit/cli",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Install the LoreKit shared-memory skill and run health checks for the LoreKit MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 three factors, each normalised to [0,1]:
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 thirds. Deliberately not tuned: with no corpus to tune against, an
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
- return (w.recency * recency + w.salience * salience + w.relevance * relevance) / total;
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