@lorekit/cli 1.44.0 → 1.45.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 +74 -20
package/package.json
CHANGED
package/src/core/lessons.mjs
CHANGED
|
@@ -111,8 +111,12 @@ const MAX_SCAN_CHARS = 4096;
|
|
|
111
111
|
// is the follow-up that replaces this.
|
|
112
112
|
export const SCOPE_READ_LIMIT = 25;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// `scope` may be injected instead of derived from `cwd` — a seam for callers
|
|
115
|
+
// that already hold a resolved scope and for tests that need a deterministic
|
|
116
|
+
// branch (deriveScope shells out to git, so the ambient branch — often a
|
|
117
|
+
// detached `HEAD` in CI — cannot exercise the branch-seeded read otherwise).
|
|
118
|
+
export async function fetchLessons(store, cwd, { now = Date.now(), scope: scopeOverride = null } = {}) {
|
|
119
|
+
const scope = scopeOverride || deriveScope(cwd);
|
|
116
120
|
// Issued BEFORE the per-scope read loop and awaited after it. Nothing in the
|
|
117
121
|
// inventory depends on the loop, so awaiting it afterwards would cost a
|
|
118
122
|
// remote store one extra SERIAL round-trip on the session-start path; started
|
|
@@ -177,21 +181,23 @@ export async function fetchLessons(store, cwd, { now = Date.now() } = {}) {
|
|
|
177
181
|
// narrow scope should instead be guaranteed floor space, that is a weighting
|
|
178
182
|
// change in `rankLessons`, not something to re-derive here.
|
|
179
183
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
184
|
+
// The rank options (see `sessionRankOpts`): a relevance query distilled from
|
|
185
|
+
// the branch NAME, at the DEFAULT weight, so a session on `feat/embedding-…`
|
|
186
|
+
// nudges embedding lessons up. Relevance only ever LIFTS an on-topic lesson —
|
|
187
|
+
// a non-matching lesson scores relevance 0, so a branch that matches nothing
|
|
188
|
+
// does not reorder the read — which is why it need not (and must not, per the
|
|
189
|
+
// Σweights normalisation) be damped by a smaller weight. A trunk branch or
|
|
190
|
+
// detached HEAD yields no terms, and the read is recency + salience exactly as
|
|
191
|
+
// before. `scopeOrder` is passed explicitly rather than left to the scorer's
|
|
192
|
+
// first-appearance default — the hierarchy is `readOrder`'s to state, not an
|
|
193
|
+
// artefact of how this function happens to build its array.
|
|
186
194
|
// ONE options object feeds both the ranking and the diversification below, so
|
|
187
|
-
// the two can never drift
|
|
188
|
-
// score to seed the MMR
|
|
189
|
-
//
|
|
190
|
-
// order
|
|
191
|
-
// that agreement structural rather than a thing two call sites have to keep in
|
|
192
|
-
// step by hand. `k` is diversification-only; `scopeOrder` is ranking-only and
|
|
195
|
+
// the two can never drift on terms, weights OR the `now` clock:
|
|
196
|
+
// `diversifyRankedLessons` recomputes each entry's score to seed the MMR
|
|
197
|
+
// objective, and scored with different options those would not line up with the
|
|
198
|
+
// sorted order. `k` is diversification-only; `scopeOrder` is ranking-only and
|
|
193
199
|
// simply ignored by the diversifier's destructuring.
|
|
194
|
-
const rankOpts =
|
|
200
|
+
const rankOpts = sessionRankOpts(scope, now);
|
|
195
201
|
const ranked = rankLessons(winners, rankOpts);
|
|
196
202
|
|
|
197
203
|
// AUDIENCE CAP before diversification: no single self-improvement loop may
|
|
@@ -274,11 +280,12 @@ export async function fetchLessons(store, cwd, { now = Date.now() } = {}) {
|
|
|
274
280
|
// session-start read. It seeds with the top-ranked lesson (score is still
|
|
275
281
|
// 0.7 of the objective) and only spends the remaining 0.3 pushing down a
|
|
276
282
|
// lesson that repeats one already shown — so the best lesson stays first and
|
|
277
|
-
// the set stops being a wall of duplicates.
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
283
|
+
// the set stops being a wall of duplicates. Spreading `rankOpts` here (rather
|
|
284
|
+
// than restating terms/weights) is what keeps the diversifier's recomputed
|
|
285
|
+
// scores in agreement with the `rankLessons` sort above — same terms, same
|
|
286
|
+
// branch-relevance weight, same `now`. The scope map and `applicable` still
|
|
287
|
+
// read from `ranked` — the map is a pointer to what EXISTS per scope, a
|
|
288
|
+
// question diversification does not change.
|
|
282
289
|
//
|
|
283
290
|
// `applicable` is the honest denominator for the header — how many the reader
|
|
284
291
|
// has, as opposed to how many fitted. It is counted BEFORE the ceiling, so
|
|
@@ -541,6 +548,53 @@ export function distilTerms(text) {
|
|
|
541
548
|
return terms;
|
|
542
549
|
}
|
|
543
550
|
|
|
551
|
+
// Single-segment branch names that carry no topic: the trunk names a session is
|
|
552
|
+
// most often on. A `<segment>/…` branch always has a leading type/author segment
|
|
553
|
+
// (`feat`, `fix`, `claude`, `dependabot`, a username) that is never the topic —
|
|
554
|
+
// see `branchQueryTerms` — so those words don't need listing here; this set is
|
|
555
|
+
// only consulted for a branch with NO `/`.
|
|
556
|
+
const TRUNK_BRANCHES = new Set(['main', 'master', 'develop', 'trunk', 'head']);
|
|
557
|
+
|
|
558
|
+
// Distil a relevance query from the branch NAME only — owner/repo never enters,
|
|
559
|
+
// because `deriveScope` keeps the raw branch in `scope.branch`. The leading
|
|
560
|
+
// `/`-segment of a branch is a type or author by convention (`feat/…`,
|
|
561
|
+
// `dependabot/…`, `alice/…`) and never the topic, so it is dropped WHOLESALE when
|
|
562
|
+
// a `/` is present; the DESCRIPTION is then tokenised by the shared `distilTerms`
|
|
563
|
+
// (so `MIN_TERM_LEN`, dedupe and the FTS-safe shape apply). A word like `release`
|
|
564
|
+
// survives when it is in the description (`feat/release-notes`), because only the
|
|
565
|
+
// FIRST segment is removed. Empty for a bare trunk name, a detached `HEAD`, or no
|
|
566
|
+
// git — the read then behaves exactly as before. Pure and total.
|
|
567
|
+
export function branchQueryTerms(scope) {
|
|
568
|
+
const branch = scope && typeof scope.branch === 'string' ? scope.branch : '';
|
|
569
|
+
if (!branch || branch === 'HEAD') return [];
|
|
570
|
+
const slash = branch.indexOf('/');
|
|
571
|
+
if (slash === -1) {
|
|
572
|
+
// No prefix segment: a bare trunk name carries no topic; anything else is
|
|
573
|
+
// its own description.
|
|
574
|
+
return TRUNK_BRANCHES.has(branch.toLowerCase()) ? [] : distilTerms(branch);
|
|
575
|
+
}
|
|
576
|
+
return distilTerms(branch.slice(slash + 1));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// The rank options for a session-start read of `scope` at `now` — THE wiring
|
|
580
|
+
// seam, so the branch-seeding is unit-testable without a git checkout. The branch
|
|
581
|
+
// query rides at the DEFAULT relevance weight, exactly like the prompt/failure
|
|
582
|
+
// paths: it only ever LIFTS an on-topic lesson (a non-matching lesson scores
|
|
583
|
+
// relevance 0, so a branch that matches nothing is byte-for-byte the old read),
|
|
584
|
+
// and it is deliberately NOT damped by a smaller weight — reducing one factor's
|
|
585
|
+
// weight shrinks the normaliser (Σweights) and rescales every score, which then
|
|
586
|
+
// distorts the unscaled Jaccard term in `selectDiverse`'s MMR even for lessons
|
|
587
|
+
// the branch never matched. `fetchLessons` feeds the ONE object this returns to
|
|
588
|
+
// both `rankLessons` and (spread) the diversifier, so their scores agree on
|
|
589
|
+
// terms and the clock.
|
|
590
|
+
export function sessionRankOpts(scope, now) {
|
|
591
|
+
return {
|
|
592
|
+
terms: branchQueryTerms(scope),
|
|
593
|
+
now,
|
|
594
|
+
scopeOrder: scope && scope.readOrder ? scope.readOrder : null,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
544
598
|
// De-duplicate store-search hits by `scope::key` and cap them, PRESERVING the
|
|
545
599
|
// store's order — which is NOT relevance ordering: the remote store filters by
|
|
546
600
|
// FTS but orders by `updated_at desc` (recency), and the local one yields scope
|