@lorekit/cli 1.42.0 → 1.43.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/core/lessons.mjs +29 -3
- package/src/lessons-pure.mjs +39 -0
package/package.json
CHANGED
package/src/core/lessons.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { deriveScope } from '../scope.mjs';
|
|
|
12
12
|
// the injected set is chosen by ONE scorer, and a future `memory.relevant` verb
|
|
13
13
|
// must be able to reuse it rather than grow a second ranking with its own idea
|
|
14
14
|
// of what "most useful" means.
|
|
15
|
-
import { resolvePrecedence, rankLessons } from '../lessons-pure.mjs';
|
|
15
|
+
import { resolvePrecedence, rankLessons, diversifyRankedLessons } from '../lessons-pure.mjs';
|
|
16
16
|
// The store's own scope inventory, normalised — the SAME helper `memory.scopes`
|
|
17
17
|
// uses, so the map and the MCP tool cannot disagree about what a scope holds or
|
|
18
18
|
// about what a failed enumeration looks like.
|
|
@@ -167,7 +167,16 @@ export async function fetchLessons(store, cwd, { now = Date.now() } = {}) {
|
|
|
167
167
|
// first-appearance default — they agree today, but the hierarchy is
|
|
168
168
|
// `readOrder`'s to state, not an artefact of how this function happens to
|
|
169
169
|
// build its array.
|
|
170
|
-
|
|
170
|
+
// ONE options object feeds both the ranking and the diversification below, so
|
|
171
|
+
// the two can never drift: `diversifyRankedLessons` recomputes each entry's
|
|
172
|
+
// score to seed the MMR objective, and if its `terms`/`now`/`weights` differed
|
|
173
|
+
// from what `rankLessons` sorted on, those scores would not line up with the
|
|
174
|
+
// order — the near-identical `now` clock especially. Sharing the object makes
|
|
175
|
+
// that agreement structural rather than a thing two call sites have to keep in
|
|
176
|
+
// step by hand. `k` is diversification-only; `scopeOrder` is ranking-only and
|
|
177
|
+
// simply ignored by the diversifier's destructuring.
|
|
178
|
+
const rankOpts = { terms: [], now, scopeOrder: scope.readOrder };
|
|
179
|
+
const ranked = rankLessons(winners, rankOpts);
|
|
171
180
|
|
|
172
181
|
// ── the scope map: EXACT counts when the store can enumerate ───────────────
|
|
173
182
|
//
|
|
@@ -219,12 +228,29 @@ export async function fetchLessons(store, cwd, { now = Date.now() } = {}) {
|
|
|
219
228
|
? scopeInventoryFromStore(inventory.scopes, scope.readOrder, derivedCounts)
|
|
220
229
|
: derivedCounts;
|
|
221
230
|
|
|
231
|
+
// DIVERSIFY before the ceiling, so the budget is not spent on near-identical
|
|
232
|
+
// lessons. Ranking answers "which lessons score highest"; on an active repo
|
|
233
|
+
// the highest cluster is often one task's iteration log — a dozen
|
|
234
|
+
// `review-outcomes::pr395-it{3,4,5}` rows that score alike AND read alike, so
|
|
235
|
+
// a plain top-N hands the reader the same lesson several times and evicts the
|
|
236
|
+
// variety underneath. `diversifyRankedLessons` applies the SAME MMR
|
|
237
|
+
// (`selectDiverse`, λ=0.7 lexical Jaccard) the hosted `order=rank` path
|
|
238
|
+
// already uses, which was defined and exported here but never wired into the
|
|
239
|
+
// session-start read. It seeds with the top-ranked lesson (score is still
|
|
240
|
+
// 0.7 of the objective) and only spends the remaining 0.3 pushing down a
|
|
241
|
+
// lesson that repeats one already shown — so the best lesson stays first and
|
|
242
|
+
// the set stops being a wall of duplicates. `terms: []` matches the
|
|
243
|
+
// `rankLessons` call above (relevance contributes nothing at session start),
|
|
244
|
+
// which the scores MUST agree with. The scope map and `applicable` still read
|
|
245
|
+
// from `ranked` — the map is a pointer to what EXISTS per scope, a question
|
|
246
|
+
// diversification does not change.
|
|
247
|
+
//
|
|
222
248
|
// `applicable` is the honest denominator for the header — how many the reader
|
|
223
249
|
// has, as opposed to how many fitted. It is counted BEFORE the ceiling, so
|
|
224
250
|
// "8 of 50" stays true no matter how the render is bounded.
|
|
225
251
|
return {
|
|
226
252
|
scope,
|
|
227
|
-
lessons: ranked
|
|
253
|
+
lessons: diversifyRankedLessons(ranked, { ...rankOpts, k: HARD_LESSON_CEILING }),
|
|
228
254
|
scopeCounts,
|
|
229
255
|
applicable: ranked.length,
|
|
230
256
|
};
|
package/src/lessons-pure.mjs
CHANGED
|
@@ -706,3 +706,42 @@ export function rankLessons(entries = [], {
|
|
|
706
706
|
return scored.map((s) => s.entry);
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Rank-then-diversify in one call — the `.mjs` twin's convenience for what the
|
|
711
|
+
* TS twin gets for free by carrying `{ entry, score }` pairs into `selectDiverse`.
|
|
712
|
+
*
|
|
713
|
+
* `selectDiverse` needs a parallel `scores` array, and those scores MUST be the
|
|
714
|
+
* same set-relative values the list was sorted on: the salience factor is
|
|
715
|
+
* normalised against the max `seen_count` in the candidate SET, so a score
|
|
716
|
+
* recomputed over a different population would not line up with the order. This
|
|
717
|
+
* helper recomputes the scores over exactly the list it diversifies, beside the
|
|
718
|
+
* unexported `seenCountFrom`/`scoreWithTerms`, so callers can apply MMR without
|
|
719
|
+
* reconstructing that alignment (and without `seenCountFrom` leaking out).
|
|
720
|
+
*
|
|
721
|
+
* `entries` is expected to be `rankLessons` output (best-first) so the seed of
|
|
722
|
+
* the greedy MMR is the top-ranked lesson; the pass-through
|
|
723
|
+
* `terms`/`weights`/`halfLifeDays`/`now` MUST match the `rankLessons` call that
|
|
724
|
+
* produced it, or the recomputed scores diverge from the sort. `k` caps the
|
|
725
|
+
* returned count (default: all). Empty/degenerate input returns `[]`.
|
|
726
|
+
*/
|
|
727
|
+
export function diversifyRankedLessons(entries = [], {
|
|
728
|
+
terms = [],
|
|
729
|
+
now = Date.now(),
|
|
730
|
+
weights = DEFAULT_RANK_WEIGHTS,
|
|
731
|
+
halfLifeDays = RECENCY_HALF_LIFE_DAYS,
|
|
732
|
+
k = Infinity,
|
|
733
|
+
lambda,
|
|
734
|
+
} = {}) {
|
|
735
|
+
const list = Array.isArray(entries) ? entries.filter((e) => e && typeof e === 'object') : [];
|
|
736
|
+
if (list.length === 0) return [];
|
|
737
|
+
const termSet = distinctTerms(terms);
|
|
738
|
+
let maxSeenCount = 0;
|
|
739
|
+
for (const e of list) maxSeenCount = Math.max(maxSeenCount, seenCountFrom(e));
|
|
740
|
+
const scores = list.map((e) => scoreWithTerms(e, termSet, { now, weights, maxSeenCount, halfLifeDays }));
|
|
741
|
+
// `numberOr` is the module's coercion convention: a non-finite `k` (the
|
|
742
|
+
// `Infinity` default, `null`, or a stringy `'40'`) resolves to a real cap —
|
|
743
|
+
// the default falls through to the whole list, `'40'` becomes 40 — rather than
|
|
744
|
+
// silently returning everything on a shape a caller plausibly passes.
|
|
745
|
+
const limit = numberOr(k, list.length);
|
|
746
|
+
return selectDiverse(list, limit, { scores, lambda });
|
|
747
|
+
}
|