claude-mem-lite 3.96.0 → 3.97.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- package/utils.mjs +184 -36
package/hook-memory.mjs
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// claude-mem-lite — Semantic Memory Injection
|
|
2
2
|
// Search past observations for relevant memories to inject as context at user-prompt time.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
relaxFtsQueryToOr,
|
|
6
|
+
debugCatch,
|
|
7
|
+
truncate,
|
|
8
|
+
OBS_BM25,
|
|
9
|
+
notLowSignalTitleClause,
|
|
10
|
+
noisePenaltyClause,
|
|
11
|
+
tokenizeHandoff,
|
|
12
|
+
HANDOFF_STOP_WORDS,
|
|
13
|
+
extractCjkKeywords,
|
|
14
|
+
neutralizeContextDelimiters,
|
|
15
|
+
} from './utils.mjs';
|
|
5
16
|
import { upsFtsQuery } from './lib/ups-query.mjs';
|
|
6
17
|
import { citeFactorJs, TYPE_QUALITY, TYPE_QUALITY_DEFAULT } from './scoring-sql.mjs';
|
|
7
18
|
import { liveObsFilterSql } from './lib/inject-search-core.mjs';
|
|
@@ -160,10 +171,14 @@ function getCrossProjectBoost() {
|
|
|
160
171
|
}
|
|
161
172
|
function extractQueryTerms(text) {
|
|
162
173
|
if (!text) return [];
|
|
163
|
-
const ascii = tokenizeHandoff(text).filter(t => !HANDOFF_STOP_WORDS.has(t));
|
|
174
|
+
const ascii = tokenizeHandoff(text).filter((t) => !HANDOFF_STOP_WORDS.has(t));
|
|
164
175
|
let cjk = [];
|
|
165
|
-
try {
|
|
166
|
-
|
|
176
|
+
try {
|
|
177
|
+
cjk = extractCjkKeywords(text) || [];
|
|
178
|
+
} catch {
|
|
179
|
+
/* CJK extraction best-effort */
|
|
180
|
+
}
|
|
181
|
+
return [...new Set([...ascii, ...cjk.map((t) => String(t).toLowerCase())])];
|
|
167
182
|
}
|
|
168
183
|
// v2.41: hay spans every FTS column whose BM25 weight is >=5 in OBS_BM25
|
|
169
184
|
// (title=10, subtitle=5, narrative=5, lesson_learned=8). Pre-v2.41 was only
|
|
@@ -176,7 +191,8 @@ const COVERAGE_NARRATIVE_PREFIX = 400;
|
|
|
176
191
|
function candidateCoverage(row, queryTerms) {
|
|
177
192
|
if (queryTerms.length === 0) return 1.0;
|
|
178
193
|
const narrativeHead = (row.narrative || '').slice(0, COVERAGE_NARRATIVE_PREFIX);
|
|
179
|
-
const hay =
|
|
194
|
+
const hay =
|
|
195
|
+
`${row.title || ''} ${row.subtitle || ''} ${row.lesson_learned || ''} ${narrativeHead}`.toLowerCase();
|
|
180
196
|
let hits = 0;
|
|
181
197
|
for (const t of queryTerms) {
|
|
182
198
|
if (/[^ -~]/.test(t)) {
|
|
@@ -191,7 +207,6 @@ function candidateCoverage(row, queryTerms) {
|
|
|
191
207
|
return hits / queryTerms.length;
|
|
192
208
|
}
|
|
193
209
|
|
|
194
|
-
|
|
195
210
|
// P1: stale-obs verify-before-use threshold. An injected obs older than this
|
|
196
211
|
// AND carrying file paths is flagged so Claude is reminded to grep/Read the
|
|
197
212
|
// referenced code before applying the lesson — code may have moved or been
|
|
@@ -217,15 +232,19 @@ export function formatMemoryLine(obs) {
|
|
|
217
232
|
// citation-decay denominator (its promote/demote loop was silently dead).
|
|
218
233
|
const lessonTag = obs.lesson_learned ? ` | Lesson: ${truncate(obs.lesson_learned, 200)}` : '';
|
|
219
234
|
let staleHint = '';
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
if (
|
|
236
|
+
typeof obs.created_at_epoch === 'number' &&
|
|
237
|
+
Date.now() - obs.created_at_epoch > STALE_OBS_THRESHOLD_MS &&
|
|
238
|
+
hasFilePaths(obs.files_modified)
|
|
239
|
+
) {
|
|
223
240
|
staleHint = ' [verify-before-use]';
|
|
224
241
|
}
|
|
225
242
|
// Defang any literal block-delimiter tag in title/lesson so it can't prematurely close
|
|
226
243
|
// the <memory-context> block this line is injected into (parity with hook-context's
|
|
227
244
|
// <claude-mem-context> defense).
|
|
228
|
-
return neutralizeContextDelimiters(
|
|
245
|
+
return neutralizeContextDelimiters(
|
|
246
|
+
`- [${obs.type}] ${truncate(obs.title, 80)}${lessonTag} (#${obs.id})${staleHint}`,
|
|
247
|
+
);
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
function hasFilePaths(filesModified) {
|
|
@@ -268,7 +287,13 @@ function hasFilePaths(filesModified) {
|
|
|
268
287
|
* BOTH arms to see one store state, which is achieved by ordering (arm B first, and
|
|
269
288
|
* it writes nothing) rather than by isolation.
|
|
270
289
|
*/
|
|
271
|
-
export function searchRelevantMemories(
|
|
290
|
+
export function searchRelevantMemories(
|
|
291
|
+
db,
|
|
292
|
+
userPrompt,
|
|
293
|
+
project,
|
|
294
|
+
excludeIds = [],
|
|
295
|
+
{ counterfactual = false } = {},
|
|
296
|
+
) {
|
|
272
297
|
// Min-length guard is English-centric: 5 chars ≈ one short English word. A CJK
|
|
273
298
|
// query is meaningful at 2 chars (状态/架构) and most real Chinese queries are
|
|
274
299
|
// 2-4 chars (状态管理, 召回率, 熔断降级) — the bare `.length < 5` silently
|
|
@@ -290,7 +315,10 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
290
315
|
// v2.41 metrics: record timing + candidate/filter/return counts per call.
|
|
291
316
|
// Gated by CLAUDE_MEM_METRICS=1 — no-op when disabled (zero hot-path cost).
|
|
292
317
|
const _t0 = Date.now();
|
|
293
|
-
let _candidates = 0,
|
|
318
|
+
let _candidates = 0,
|
|
319
|
+
_aboveThreshold = 0,
|
|
320
|
+
_returned = 0,
|
|
321
|
+
_orFired = false;
|
|
294
322
|
const _emit = () => {
|
|
295
323
|
if (counterfactual) return;
|
|
296
324
|
try {
|
|
@@ -302,7 +330,9 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
302
330
|
returned: _returned,
|
|
303
331
|
orFallback: _orFired,
|
|
304
332
|
});
|
|
305
|
-
} catch {
|
|
333
|
+
} catch {
|
|
334
|
+
/* metric record must not crash the caller */
|
|
335
|
+
}
|
|
306
336
|
};
|
|
307
337
|
|
|
308
338
|
try {
|
|
@@ -350,7 +380,7 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
350
380
|
// Count original search terms (AND-separated groups), not expanded synonym tokens.
|
|
351
381
|
const queryTokenCount = ftsQuery.includes(' AND ')
|
|
352
382
|
? ftsQuery.split(' AND ').length
|
|
353
|
-
: ftsQuery.split(/\s+/).filter(t => t && !t.startsWith('(') && !t.endsWith(')')).length;
|
|
383
|
+
: ftsQuery.split(/\s+/).filter((t) => t && !t.startsWith('(') && !t.endsWith(')')).length;
|
|
354
384
|
// CJK-dominant queries bypass the token-count gate: a single CJK word becomes
|
|
355
385
|
// 2-N overlapping bigrams (优化召回率 → 优化/召回/回率), inflating
|
|
356
386
|
// queryTokenCount past the gate, so the AND-too-strict query never gets the OR
|
|
@@ -369,8 +399,12 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
369
399
|
// (The two bare catches further down, around the per-row access bumps, are
|
|
370
400
|
// deliberately left bare: they are write-path and per-row, so logging them would
|
|
371
401
|
// flood the debug stream on the same corruption this one reports once.)
|
|
372
|
-
try {
|
|
373
|
-
|
|
402
|
+
try {
|
|
403
|
+
rows = selectStmt.all(orQuery, project, cutoff);
|
|
404
|
+
usedOrFallback = true;
|
|
405
|
+
} catch (e) {
|
|
406
|
+
debugCatch(e, 'injectMemory:orFallback');
|
|
407
|
+
}
|
|
374
408
|
}
|
|
375
409
|
}
|
|
376
410
|
|
|
@@ -403,11 +437,17 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
403
437
|
if (orQuery && (queryIsCjkDominant || queryTokenCount <= orFallbackMaxTokens)) {
|
|
404
438
|
// Same reasoning as the same-project OR fallback above: a fault here silently
|
|
405
439
|
// drops the cross-project half of the injection.
|
|
406
|
-
try {
|
|
407
|
-
|
|
440
|
+
try {
|
|
441
|
+
crossRows = crossStmt.all(orQuery, project, cutoff);
|
|
442
|
+
crossUsedOr = true;
|
|
443
|
+
} catch (e) {
|
|
444
|
+
debugCatch(e, 'injectMemory:crossOrFallback');
|
|
445
|
+
}
|
|
408
446
|
}
|
|
409
447
|
}
|
|
410
|
-
} catch (e) {
|
|
448
|
+
} catch (e) {
|
|
449
|
+
debugCatch(e, 'crossProjectSearch');
|
|
450
|
+
}
|
|
411
451
|
|
|
412
452
|
// Merge and score: same-project full weight, cross-project (default 0.7x).
|
|
413
453
|
// v2.41: cross-project penalty is env-overridable via MEM_CROSS_PROJECT_BOOST
|
|
@@ -419,40 +459,48 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
419
459
|
// OR-fallback results get 0.4x penalty — they matched individual words, not the full intent
|
|
420
460
|
// v26 P0: noise_penalty (from SQL) shrinks high-inject/low-cite rows.
|
|
421
461
|
const crossPenalty = getCrossProjectBoost();
|
|
422
|
-
const allRows = [
|
|
462
|
+
const allRows = [
|
|
463
|
+
...rows.map((r) => ({ ...r, _or: usedOrFallback })),
|
|
464
|
+
...crossRows.map((r) => ({ ...r, _or: crossUsedOr })),
|
|
465
|
+
];
|
|
423
466
|
const scored = allRows
|
|
424
|
-
.filter(r => !excludeSet.has(r.id))
|
|
425
|
-
.map(r => {
|
|
467
|
+
.filter((r) => !excludeSet.has(r.id))
|
|
468
|
+
.map((r) => {
|
|
426
469
|
const crossProjectPenalty = r.project === project ? 1.0 : crossPenalty;
|
|
427
470
|
const orFallbackPenalty = r._or ? 0.4 : 1.0;
|
|
428
471
|
const noisePenalty = typeof r.noise_penalty === 'number' ? r.noise_penalty : 1.0;
|
|
429
472
|
const citeFactor = citeFactorJs(r);
|
|
430
473
|
return {
|
|
431
474
|
...r,
|
|
432
|
-
score:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
475
|
+
score:
|
|
476
|
+
Math.abs(r.relevance) *
|
|
477
|
+
(TYPE_QUALITY[r.type] || TYPE_QUALITY_DEFAULT) *
|
|
478
|
+
(r.lesson_learned ? 1.5 : 1.0) *
|
|
479
|
+
(r.importance >= 2 ? 1.0 : 0.6) *
|
|
480
|
+
crossProjectPenalty *
|
|
481
|
+
orFallbackPenalty *
|
|
482
|
+
noisePenalty *
|
|
483
|
+
citeFactor,
|
|
440
484
|
};
|
|
441
485
|
})
|
|
442
486
|
.sort((a, b) => b.score - a.score);
|
|
443
487
|
|
|
444
488
|
// Adaptive threshold: scales with corpus size to filter noise.
|
|
445
489
|
// Each result must individually exceed the threshold (not just the top one).
|
|
446
|
-
const obsCount =
|
|
447
|
-
|
|
448
|
-
|
|
490
|
+
const obsCount =
|
|
491
|
+
db
|
|
492
|
+
.prepare(`SELECT COUNT(*) as c FROM observations WHERE project = ? AND ${liveObsFilterSql('')}`)
|
|
493
|
+
.get(project)?.c || 0;
|
|
449
494
|
const { TINY, SMALL, MEDIUM, LARGE } = BM25_THRESHOLD;
|
|
450
495
|
const threshold = obsCount < 5 ? TINY : obsCount < 100 ? SMALL : obsCount < 500 ? MEDIUM : LARGE;
|
|
451
496
|
_candidates = scored.length;
|
|
452
497
|
_orFired = usedOrFallback || crossUsedOr;
|
|
453
|
-
const aboveThreshold = scored.filter(r => r.score >= threshold);
|
|
498
|
+
const aboveThreshold = scored.filter((r) => r.score >= threshold);
|
|
454
499
|
_aboveThreshold = aboveThreshold.length;
|
|
455
|
-
if (aboveThreshold.length === 0) {
|
|
500
|
+
if (aboveThreshold.length === 0) {
|
|
501
|
+
_emit();
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
456
504
|
|
|
457
505
|
// v27: term-coverage filter — drop candidates whose title+lesson_learned
|
|
458
506
|
// covers <threshold of the query's significant terms. Skipped for
|
|
@@ -463,8 +511,13 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
463
511
|
if (coverageThreshold > 0) {
|
|
464
512
|
const queryTerms = extractQueryTerms(userPrompt);
|
|
465
513
|
if (queryTerms.length >= COVERAGE_MIN_QUERY_TERMS) {
|
|
466
|
-
coverageFiltered = aboveThreshold.filter(
|
|
467
|
-
|
|
514
|
+
coverageFiltered = aboveThreshold.filter(
|
|
515
|
+
(r) => candidateCoverage(r, queryTerms) >= coverageThreshold,
|
|
516
|
+
);
|
|
517
|
+
if (coverageFiltered.length === 0) {
|
|
518
|
+
_emit();
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
468
521
|
}
|
|
469
522
|
}
|
|
470
523
|
|
|
@@ -499,10 +552,12 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [],
|
|
|
499
552
|
if (!counterfactual) {
|
|
500
553
|
const now = Date.now();
|
|
501
554
|
const bumpStmt = db.prepare(
|
|
502
|
-
'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?'
|
|
555
|
+
'UPDATE observations SET injection_count = COALESCE(injection_count, 0) + 1, last_injected_at = ? WHERE id = ?',
|
|
503
556
|
);
|
|
504
557
|
for (const r of result) {
|
|
505
|
-
try {
|
|
558
|
+
try {
|
|
559
|
+
bumpStmt.run(now, r.id);
|
|
560
|
+
} catch {}
|
|
506
561
|
}
|
|
507
562
|
}
|
|
508
563
|
|
|
@@ -613,7 +668,9 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
|
|
|
613
668
|
// guaranteed one. There are zero such collisions live, so it changes no behaviour here;
|
|
614
669
|
// it makes the guarantee hold on corpora nobody has seen.
|
|
615
670
|
try {
|
|
616
|
-
rows = db
|
|
671
|
+
rows = db
|
|
672
|
+
.prepare(
|
|
673
|
+
`
|
|
617
674
|
SELECT id, title, lesson_learned, importance
|
|
618
675
|
FROM observations
|
|
619
676
|
WHERE project = ?
|
|
@@ -625,12 +682,18 @@ export function rankImperativeCandidates(db, userPrompt, project, excludeIds = [
|
|
|
625
682
|
AND (? IS NULL OR created_at_epoch <= ?)
|
|
626
683
|
ORDER BY importance DESC, created_at_epoch DESC, id DESC
|
|
627
684
|
LIMIT ${IMPERATIVE_POOL_BACKSTOP}
|
|
628
|
-
|
|
629
|
-
|
|
685
|
+
`,
|
|
686
|
+
)
|
|
687
|
+
.all(project, epochTo, epochTo);
|
|
688
|
+
} catch {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
630
691
|
const out = [];
|
|
631
692
|
for (const r of rows) {
|
|
632
693
|
if (exclude.has(r.id)) continue;
|
|
633
|
-
const overlap = extractIdents(`${r.lesson_learned} ${r.title || ''}`).filter((id) =>
|
|
694
|
+
const overlap = extractIdents(`${r.lesson_learned} ${r.title || ''}`).filter((id) =>
|
|
695
|
+
promptIdents.has(id),
|
|
696
|
+
).length;
|
|
634
697
|
if (overlap === 0) continue;
|
|
635
698
|
const score = (r.importance || 2) * overlap;
|
|
636
699
|
out.push({ id: r.id, lesson_learned: r.lesson_learned, importance: r.importance || 2, overlap, score });
|