@xdarkicex/openclaw-memory-libravdb 1.4.3 → 1.4.5
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/README.md +76 -16
- package/docs/README.md +3 -12
- package/docs/architecture.md +68 -153
- package/docs/contributing.md +1 -2
- package/openclaw.plugin.json +64 -1
- package/package.json +2 -2
- package/src/cli.ts +34 -0
- package/src/comparison-experiments.ts +128 -0
- package/src/context-engine.ts +286 -72
- package/src/dream-promotion.ts +492 -0
- package/src/dream-routing.ts +40 -0
- package/src/index.ts +16 -1
- package/src/markdown-hash.ts +104 -0
- package/src/markdown-ingest.ts +627 -0
- package/src/memory-runtime.ts +32 -9
- package/src/scoring.ts +6 -3
- package/src/temporal.ts +657 -80
- package/src/types.ts +48 -0
- package/docs/ast-v2.md +0 -167
- package/docs/ast.md +0 -70
- package/docs/compaction-evaluation.md +0 -182
- package/docs/continuity.md +0 -708
- package/docs/elevated-guidance.md +0 -258
- package/docs/gating.md +0 -134
- package/docs/implementation.md +0 -447
- package/docs/mathematics-v2.md +0 -1879
- package/docs/mathematics.md +0 -695
package/src/temporal.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { estimateTokens } from "./tokens.js";
|
|
1
2
|
import type { SearchResult } from "./types.js";
|
|
3
|
+
import {
|
|
4
|
+
createComparisonProfileSummary,
|
|
5
|
+
resolveComparisonExperimentConfig,
|
|
6
|
+
type ComparisonExperimentConfig,
|
|
7
|
+
type ComparisonProfileSummary,
|
|
8
|
+
} from "./comparison-experiments.js";
|
|
2
9
|
|
|
3
10
|
const TEMPORAL_PATTERN_WEIGHTS: Array<{ label: string; weight: number; patterns: RegExp[] }> = [
|
|
4
11
|
{
|
|
@@ -40,6 +47,7 @@ const TEMPORAL_ANCHOR_PATTERNS: RegExp[] = [
|
|
|
40
47
|
/\b(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
|
|
41
48
|
/\b(?:today|yesterday|tomorrow|last\s+(?:week|month|year|night|saturday|sunday)|next\s+(?:week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)|mid-?[a-z]+)\b/gi,
|
|
42
49
|
/\b\d{1,2}:\d{2}(?:\s?[ap]m)?\b/gi,
|
|
50
|
+
/\b(?:19|20)\d{2}\b/g,
|
|
43
51
|
/\b\d{10,13}\b/g,
|
|
44
52
|
];
|
|
45
53
|
|
|
@@ -106,6 +114,62 @@ const TEMPORAL_SLOT_STOPWORDS = new Set([
|
|
|
106
114
|
"i",
|
|
107
115
|
]);
|
|
108
116
|
|
|
117
|
+
const COMPARISON_GENERIC_SLOT_PREFIXES = new Set([
|
|
118
|
+
"trip",
|
|
119
|
+
"event",
|
|
120
|
+
"device",
|
|
121
|
+
"thing",
|
|
122
|
+
"item",
|
|
123
|
+
"time",
|
|
124
|
+
"place",
|
|
125
|
+
"activity",
|
|
126
|
+
"experience",
|
|
127
|
+
"job",
|
|
128
|
+
"role",
|
|
129
|
+
"project",
|
|
130
|
+
"task",
|
|
131
|
+
"purchase",
|
|
132
|
+
"plan",
|
|
133
|
+
"change",
|
|
134
|
+
"decision",
|
|
135
|
+
"move",
|
|
136
|
+
"visit",
|
|
137
|
+
"meeting",
|
|
138
|
+
"session",
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const COMPARISON_GENERIC_AFFILIATION_TERMS = new Set([
|
|
142
|
+
...COMPARISON_GENERIC_SLOT_PREFIXES,
|
|
143
|
+
"solo",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const COMPARISON_FIRST_PERSON_CLAUSE_PATTERNS: RegExp[] = [
|
|
147
|
+
/\bi\b/gi,
|
|
148
|
+
/\bi'm\b/gi,
|
|
149
|
+
/\bi've\b/gi,
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const COMPARISON_PROSPECTIVE_PERSONAL_PATTERNS: RegExp[] = [
|
|
153
|
+
/\b(?:i\s+am\s+|i'm\s+)?considering\b/gi,
|
|
154
|
+
/\b(?:i\s+am\s+|i'm\s+)?planning\b/gi,
|
|
155
|
+
/\bi\s+am\s+thinking\s+about\b/gi,
|
|
156
|
+
/\bi'?m\s+thinking\s+about\b/gi,
|
|
157
|
+
/\bi\s+have\s+been\s+looking\b/gi,
|
|
158
|
+
/\bi'?ve\s+been\s+looking\b/gi,
|
|
159
|
+
/\bi\s+am\s+looking\s+(?:at|into)\b/gi,
|
|
160
|
+
/\bi'?m\s+looking\s+(?:at|into)\b/gi,
|
|
161
|
+
/\bi\s+want\s+to\b/gi,
|
|
162
|
+
/\bi\s+would\s+like\s+to\b/gi,
|
|
163
|
+
/\bi\s+am\s+hoping\s+to\b/gi,
|
|
164
|
+
/\bi'?m\s+hoping\s+to\b/gi,
|
|
165
|
+
/\bi\s+am\s+going\s+to\s+(?:visit|go|try|do)\b/gi,
|
|
166
|
+
/\bi'?m\s+going\s+to\s+(?:visit|go|try|do)\b/gi,
|
|
167
|
+
/\bi\s+am\s+trying\s+to\s+decide\b/gi,
|
|
168
|
+
/\bi'?m\s+trying\s+to\s+decide\b/gi,
|
|
169
|
+
/\bi\s+am\s+trying\s+to\s+(?:plan|figure)\b/gi,
|
|
170
|
+
/\bi'?m\s+trying\s+to\s+(?:plan|figure)\b/gi,
|
|
171
|
+
];
|
|
172
|
+
|
|
109
173
|
export interface TemporalQuerySignal {
|
|
110
174
|
indicator: number;
|
|
111
175
|
active: boolean;
|
|
@@ -129,6 +193,19 @@ export interface TemporalRecoveryDebugCandidate {
|
|
|
129
193
|
slotMatches: string[];
|
|
130
194
|
finalScore: number;
|
|
131
195
|
rationale: string;
|
|
196
|
+
comparisonSide?: 0 | 1 | null;
|
|
197
|
+
comparisonSlot?: string;
|
|
198
|
+
comparisonSlotRecall?: number;
|
|
199
|
+
comparisonSlotPrecision?: number;
|
|
200
|
+
comparisonSlotSpecificity?: number;
|
|
201
|
+
comparisonSlotPositionWeightedRecall?: number;
|
|
202
|
+
comparisonSlotPositionWeightedPrecision?: number;
|
|
203
|
+
comparisonSlotPositionWeightedSpecificity?: number;
|
|
204
|
+
comparisonFirstPersonClauseCount?: number;
|
|
205
|
+
comparisonProspectivePersonalVerbCount?: number;
|
|
206
|
+
comparisonPlanningDensity?: number;
|
|
207
|
+
comparisonPastness?: number;
|
|
208
|
+
comparisonSideWitnessScore?: number;
|
|
132
209
|
}
|
|
133
210
|
|
|
134
211
|
export interface TemporalRecoveryRankingResult {
|
|
@@ -136,8 +213,16 @@ export interface TemporalRecoveryRankingResult {
|
|
|
136
213
|
debug: TemporalRecoveryDebugCandidate[];
|
|
137
214
|
temporalQuery: TemporalQuerySignal;
|
|
138
215
|
slots: string[];
|
|
216
|
+
comparisonCoverageApplied?: boolean;
|
|
217
|
+
comparisonCoverageSlots?: string[];
|
|
218
|
+
comparisonCoverageMinTokens?: number;
|
|
219
|
+
comparisonWitnessIds?: string[];
|
|
220
|
+
comparisonProfile?: ComparisonProfileSummary;
|
|
139
221
|
}
|
|
140
222
|
|
|
223
|
+
let activeComparisonProfile: ComparisonProfileSummary | null = null;
|
|
224
|
+
let activeComparisonExperimentConfig: ComparisonExperimentConfig | null = null;
|
|
225
|
+
|
|
141
226
|
export function detectTemporalQuerySignal(queryText: string): TemporalQuerySignal {
|
|
142
227
|
const matchedPatterns: string[] = [];
|
|
143
228
|
let weightedMatches = 0;
|
|
@@ -187,99 +272,234 @@ export function rankTemporalRecoveryCandidates(
|
|
|
187
272
|
maxSelected?: number;
|
|
188
273
|
nowMs?: number;
|
|
189
274
|
recencyLambda?: number;
|
|
275
|
+
selectionTokenBudget?: number;
|
|
190
276
|
},
|
|
191
277
|
): TemporalRecoveryRankingResult {
|
|
278
|
+
const comparisonExperiment = resolveComparisonExperimentConfig();
|
|
192
279
|
const temporalQuery = detectTemporalQuerySignal(opts.queryText);
|
|
193
280
|
const slots = extractTemporalSlots(opts.queryText);
|
|
281
|
+
const isComparisonQuery = temporalQuery.matchedPatterns.includes("first or earlier");
|
|
282
|
+
const effectiveSlots = isComparisonQuery ? filterComparisonSlots(slots) : slots;
|
|
283
|
+
const comparisonSlots = isComparisonQuery ? deriveComparisonSideSlots(effectiveSlots) : [];
|
|
194
284
|
const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
|
|
195
285
|
const now = opts.nowMs ?? Date.now();
|
|
196
286
|
const maxSelected = Math.max(1, Math.floor(opts.maxSelected ?? 3));
|
|
287
|
+
const selectionTokenBudget = Math.max(0, Math.floor(opts.selectionTokenBudget ?? Number.MAX_SAFE_INTEGER));
|
|
288
|
+
const comparisonProfile = comparisonExperiment.profilingEnabled && isComparisonQuery
|
|
289
|
+
? createComparisonProfileSummary(comparisonExperiment.ablationMode)
|
|
290
|
+
: null;
|
|
291
|
+
const previousProfile = activeComparisonProfile;
|
|
292
|
+
const previousExperimentConfig = activeComparisonExperimentConfig;
|
|
293
|
+
activeComparisonProfile = comparisonProfile;
|
|
294
|
+
activeComparisonExperimentConfig = comparisonExperiment;
|
|
295
|
+
const totalStart = comparisonProfile ? process.hrtime.bigint() : 0n;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
if (comparisonProfile) {
|
|
299
|
+
comparisonProfile.rawCandidateCount = items.length;
|
|
300
|
+
}
|
|
197
301
|
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
302
|
+
const decorateStart = comparisonProfile ? process.hrtime.bigint() : 0n;
|
|
303
|
+
const decorated = items.map((item) => {
|
|
304
|
+
const semanticScore = clamp01(typeof item.finalScore === "number" ? item.finalScore : item.score ?? 0);
|
|
305
|
+
const recencyScore = computeRecencyScore(item, now, recencyLambda);
|
|
306
|
+
const temporalAnchorDensity = getTemporalAnchorDensity(
|
|
307
|
+
`${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
|
|
308
|
+
item.text,
|
|
309
|
+
);
|
|
310
|
+
const { coverage, matches } = computeSlotCoverage(effectiveSlots, item.text);
|
|
311
|
+
const tokenEstimate = estimateTokensWithProfile(item.text);
|
|
312
|
+
const comparisonSide = comparisonSlots.length === 2
|
|
313
|
+
? computeComparisonSideAffiliation(comparisonSlots, item.text)
|
|
314
|
+
: null;
|
|
315
|
+
const comparisonMetrics = comparisonSide !== null
|
|
316
|
+
? computeComparisonSlotSpecificityMetrics(
|
|
317
|
+
comparisonSlots[comparisonSide]!,
|
|
318
|
+
item.text,
|
|
319
|
+
comparisonSlots[1 - comparisonSide],
|
|
320
|
+
)
|
|
321
|
+
: null;
|
|
322
|
+
const comparisonSideWitnessScore = comparisonMetrics
|
|
323
|
+
? comparisonMetrics.sideWitnessScore
|
|
324
|
+
: undefined;
|
|
325
|
+
const finalScore = clamp01(isComparisonQuery
|
|
326
|
+
? computeComparisonFinalScore({
|
|
327
|
+
semanticScore,
|
|
328
|
+
recencyScore,
|
|
329
|
+
temporalAnchorDensity,
|
|
330
|
+
coverage,
|
|
331
|
+
comparisonSideWitnessScore,
|
|
332
|
+
temporalQueryActive: temporalQuery.active,
|
|
333
|
+
})
|
|
334
|
+
: (0.40 * semanticScore) +
|
|
335
|
+
(0.25 * recencyScore) +
|
|
336
|
+
(0.20 * temporalAnchorDensity) +
|
|
337
|
+
(0.15 * coverage) +
|
|
338
|
+
(temporalQuery.active ? 0.05 : 0));
|
|
339
|
+
return {
|
|
340
|
+
item,
|
|
341
|
+
semanticScore,
|
|
342
|
+
recencyScore,
|
|
343
|
+
temporalAnchorDensity,
|
|
344
|
+
slotCoverage: coverage,
|
|
345
|
+
slotMatches: matches,
|
|
346
|
+
tokenEstimate,
|
|
347
|
+
comparisonSide,
|
|
348
|
+
comparisonMetrics,
|
|
349
|
+
comparisonSideWitnessScore,
|
|
350
|
+
finalScore,
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
if (comparisonProfile) {
|
|
354
|
+
comparisonProfile.decorateMs += elapsedMs(decorateStart);
|
|
355
|
+
comparisonProfile.comparisonCandidateCount = decorated.filter((candidate) => candidate.comparisonSide !== null).length;
|
|
356
|
+
comparisonProfile.side0AffiliatedCount = decorated.filter((candidate) => candidate.comparisonSide === 0).length;
|
|
357
|
+
comparisonProfile.side1AffiliatedCount = decorated.filter((candidate) => candidate.comparisonSide === 1).length;
|
|
358
|
+
}
|
|
223
359
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
360
|
+
const selectedIDs = new Set<string>();
|
|
361
|
+
const coveredSlots = new Set<string>();
|
|
362
|
+
const selected: SearchResult[] = [];
|
|
363
|
+
let comparisonCoverageApplied = false;
|
|
364
|
+
let comparisonCoverageMinTokens: number | undefined;
|
|
365
|
+
let comparisonWitnessIds: string[] | undefined;
|
|
366
|
+
|
|
367
|
+
if (comparisonSlots.length === 2) {
|
|
368
|
+
const sideCandidates = comparisonSlots.map((_, sideIndex) => {
|
|
369
|
+
const candidates = decorated
|
|
370
|
+
.filter((candidate) => candidate.comparisonSide === sideIndex);
|
|
371
|
+
recordSort(candidates.length);
|
|
372
|
+
return candidates.sort((left, right) => {
|
|
373
|
+
const leftWitnessScore = left.comparisonSideWitnessScore ?? Number.NEGATIVE_INFINITY;
|
|
374
|
+
const rightWitnessScore = right.comparisonSideWitnessScore ?? Number.NEGATIVE_INFINITY;
|
|
375
|
+
if (rightWitnessScore !== leftWitnessScore) {
|
|
376
|
+
return rightWitnessScore - leftWitnessScore;
|
|
377
|
+
}
|
|
378
|
+
if (right.finalScore !== left.finalScore) {
|
|
379
|
+
return right.finalScore - left.finalScore;
|
|
380
|
+
}
|
|
381
|
+
return left.tokenEstimate - right.tokenEstimate;
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
const cheapestSideTokenSum = sideCandidates.reduce((sum, candidates) => (
|
|
385
|
+
candidates.length > 0 ? sum + candidates.reduce((best, candidate) => Math.min(best, candidate.tokenEstimate), Number.POSITIVE_INFINITY) : sum
|
|
386
|
+
), 0);
|
|
387
|
+
if (sideCandidates.every((candidates) => candidates.length > 0) && Number.isFinite(cheapestSideTokenSum)) {
|
|
388
|
+
comparisonCoverageMinTokens = cheapestSideTokenSum;
|
|
389
|
+
const bestPair = findBestComparisonCoveragePair(sideCandidates[0], sideCandidates[1], selectionTokenBudget);
|
|
390
|
+
if (bestPair) {
|
|
391
|
+
for (const candidate of bestPair) {
|
|
392
|
+
selectedIDs.add(candidate.item.id);
|
|
393
|
+
for (const slot of candidate.slotMatches) {
|
|
394
|
+
coveredSlots.add(slot);
|
|
395
|
+
}
|
|
396
|
+
selected.push({
|
|
397
|
+
...candidate.item,
|
|
398
|
+
finalScore: candidate.finalScore,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
comparisonCoverageApplied = true;
|
|
402
|
+
comparisonWitnessIds = [bestPair[0].item.id, bestPair[1].item.id];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
227
406
|
|
|
228
|
-
|
|
229
|
-
let
|
|
230
|
-
|
|
407
|
+
const greedyStart = comparisonProfile ? process.hrtime.bigint() : 0n;
|
|
408
|
+
for (let pass = selected.length; pass < maxSelected; pass += 1) {
|
|
409
|
+
let best: (typeof decorated)[number] | null = null;
|
|
410
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
411
|
+
|
|
412
|
+
for (const candidate of decorated) {
|
|
413
|
+
if (selectedIDs.has(candidate.item.id)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const marginalCoverage =
|
|
417
|
+
candidate.slotMatches.filter((slot) => !coveredSlots.has(slot)).length / Math.max(1, effectiveSlots.length);
|
|
418
|
+
const combined = candidate.finalScore + (0.25 * marginalCoverage);
|
|
419
|
+
if (combined > bestScore) {
|
|
420
|
+
best = candidate;
|
|
421
|
+
bestScore = combined;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
231
424
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
continue;
|
|
425
|
+
if (!best || bestScore < 0.12) {
|
|
426
|
+
break;
|
|
235
427
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
bestScore = combined;
|
|
428
|
+
|
|
429
|
+
selectedIDs.add(best.item.id);
|
|
430
|
+
for (const slot of best.slotMatches) {
|
|
431
|
+
coveredSlots.add(slot);
|
|
241
432
|
}
|
|
433
|
+
selected.push({
|
|
434
|
+
...best.item,
|
|
435
|
+
finalScore: best.finalScore,
|
|
436
|
+
});
|
|
242
437
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
break;
|
|
438
|
+
if (comparisonProfile) {
|
|
439
|
+
comparisonProfile.greedyFillMs += elapsedMs(greedyStart);
|
|
246
440
|
}
|
|
247
441
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
442
|
+
const remainingCandidates = decorated
|
|
443
|
+
.filter((candidate) => !selectedIDs.has(candidate.item.id));
|
|
444
|
+
recordSort(remainingCandidates.length);
|
|
445
|
+
const remaining = remainingCandidates
|
|
446
|
+
.sort((left, right) => right.finalScore - left.finalScore)
|
|
447
|
+
.map((candidate) => ({
|
|
448
|
+
...candidate.item,
|
|
449
|
+
finalScore: candidate.finalScore,
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
const ranked = [...selected, ...remaining];
|
|
453
|
+
const debugStart = comparisonProfile ? process.hrtime.bigint() : 0n;
|
|
454
|
+
const debugCandidates = [...decorated];
|
|
455
|
+
recordSort(debugCandidates.length);
|
|
456
|
+
const debug = debugCandidates
|
|
457
|
+
.sort((left, right) => right.finalScore - left.finalScore)
|
|
458
|
+
.map((candidate) => ({
|
|
459
|
+
id: candidate.item.id,
|
|
460
|
+
text: candidate.item.text,
|
|
461
|
+
selected: selectedIDs.has(candidate.item.id),
|
|
462
|
+
temporalAnchorDensity: candidate.temporalAnchorDensity,
|
|
463
|
+
semanticScore: candidate.semanticScore,
|
|
464
|
+
recencyScore: candidate.recencyScore,
|
|
465
|
+
slotCoverage: candidate.slotCoverage,
|
|
466
|
+
slotMatches: candidate.slotMatches,
|
|
467
|
+
finalScore: candidate.finalScore,
|
|
468
|
+
rationale: buildTemporalRecoveryRationale(candidate.slotCoverage, candidate.temporalAnchorDensity, candidate.semanticScore),
|
|
469
|
+
comparisonSide: candidate.comparisonSide,
|
|
470
|
+
comparisonSlot: candidate.comparisonSide !== null ? comparisonSlots[candidate.comparisonSide] : undefined,
|
|
471
|
+
comparisonSlotRecall: candidate.comparisonMetrics?.recall,
|
|
472
|
+
comparisonSlotPrecision: candidate.comparisonMetrics?.precision,
|
|
473
|
+
comparisonSlotSpecificity: candidate.comparisonMetrics?.specificity,
|
|
474
|
+
comparisonSlotPositionWeightedRecall: candidate.comparisonMetrics?.positionWeightedRecall,
|
|
475
|
+
comparisonSlotPositionWeightedPrecision: candidate.comparisonMetrics?.positionWeightedPrecision,
|
|
476
|
+
comparisonSlotPositionWeightedSpecificity: candidate.comparisonMetrics?.positionWeightedSpecificity,
|
|
477
|
+
comparisonFirstPersonClauseCount: candidate.comparisonMetrics?.firstPersonClauseCount,
|
|
478
|
+
comparisonProspectivePersonalVerbCount: candidate.comparisonMetrics?.prospectivePersonalVerbCount,
|
|
479
|
+
comparisonPlanningDensity: candidate.comparisonMetrics?.planningDensity,
|
|
480
|
+
comparisonPastness: candidate.comparisonMetrics?.pastness,
|
|
481
|
+
comparisonSideWitnessScore: candidate.comparisonSideWitnessScore,
|
|
482
|
+
}));
|
|
483
|
+
if (comparisonProfile) {
|
|
484
|
+
comparisonProfile.debugBuildMs += elapsedMs(debugStart);
|
|
485
|
+
comparisonProfile.rankTotalMs += elapsedMs(totalStart);
|
|
251
486
|
}
|
|
252
|
-
selected.push({
|
|
253
|
-
...best.item,
|
|
254
|
-
finalScore: best.finalScore,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
487
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
temporalAnchorDensity: candidate.temporalAnchorDensity,
|
|
274
|
-
semanticScore: candidate.semanticScore,
|
|
275
|
-
recencyScore: candidate.recencyScore,
|
|
276
|
-
slotCoverage: candidate.slotCoverage,
|
|
277
|
-
slotMatches: candidate.slotMatches,
|
|
278
|
-
finalScore: candidate.finalScore,
|
|
279
|
-
rationale: buildTemporalRecoveryRationale(candidate.slotCoverage, candidate.temporalAnchorDensity, candidate.semanticScore),
|
|
280
|
-
}));
|
|
281
|
-
|
|
282
|
-
return { ranked, debug, temporalQuery, slots };
|
|
488
|
+
return {
|
|
489
|
+
ranked,
|
|
490
|
+
debug,
|
|
491
|
+
temporalQuery,
|
|
492
|
+
slots: effectiveSlots,
|
|
493
|
+
comparisonCoverageApplied,
|
|
494
|
+
comparisonCoverageSlots: comparisonSlots.length === 2 ? comparisonSlots : undefined,
|
|
495
|
+
comparisonCoverageMinTokens,
|
|
496
|
+
comparisonWitnessIds,
|
|
497
|
+
comparisonProfile: comparisonProfile ?? undefined,
|
|
498
|
+
};
|
|
499
|
+
} finally {
|
|
500
|
+
activeComparisonProfile = previousProfile;
|
|
501
|
+
activeComparisonExperimentConfig = previousExperimentConfig;
|
|
502
|
+
}
|
|
283
503
|
}
|
|
284
504
|
|
|
285
505
|
export function decideTemporalSelectorGuard(
|
|
@@ -301,7 +521,10 @@ export function decideTemporalSelectorGuard(
|
|
|
301
521
|
pattern === "before or after" ||
|
|
302
522
|
pattern === "since or between"
|
|
303
523
|
);
|
|
304
|
-
|
|
524
|
+
const strongComparisonPattern = temporalQuery.matchedPatterns.some((pattern) =>
|
|
525
|
+
pattern === "first or earlier"
|
|
526
|
+
);
|
|
527
|
+
if (!strongCompositionalPattern && !strongComparisonPattern) {
|
|
305
528
|
return {
|
|
306
529
|
shouldApply: false,
|
|
307
530
|
slots,
|
|
@@ -309,6 +532,21 @@ export function decideTemporalSelectorGuard(
|
|
|
309
532
|
};
|
|
310
533
|
}
|
|
311
534
|
|
|
535
|
+
if (strongComparisonPattern && !strongCompositionalPattern) {
|
|
536
|
+
if (slots.length < 1 || slots.length > 4) {
|
|
537
|
+
return {
|
|
538
|
+
shouldApply: false,
|
|
539
|
+
slots,
|
|
540
|
+
reason: "comparison query did not resolve to a sensible number of slots",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
shouldApply: true,
|
|
545
|
+
slots,
|
|
546
|
+
reason: "comparison query with temporal entity extraction",
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
312
550
|
if (slots.length !== 2) {
|
|
313
551
|
return {
|
|
314
552
|
shouldApply: false,
|
|
@@ -329,10 +567,7 @@ export function resetTemporalCachesForTest(): void {
|
|
|
329
567
|
}
|
|
330
568
|
|
|
331
569
|
function extractTemporalSlots(text: string): string[] {
|
|
332
|
-
const clauses = text
|
|
333
|
-
.split(/(?:\bafter\b|\bbefore\b|\bbetween\b|\bor\b|\band\b|\bthen\b|[?.!,;]+)/i)
|
|
334
|
-
.map((part) => part.trim())
|
|
335
|
-
.filter((part) => part.length > 0);
|
|
570
|
+
const clauses = splitTemporalClauses(text);
|
|
336
571
|
const slots = new Set<string>();
|
|
337
572
|
|
|
338
573
|
for (const clause of clauses) {
|
|
@@ -386,6 +621,235 @@ function computeSlotCoverage(slots: string[], candidateText: string): { coverage
|
|
|
386
621
|
};
|
|
387
622
|
}
|
|
388
623
|
|
|
624
|
+
function filterComparisonSlots(slots: string[]): string[] {
|
|
625
|
+
const filtered = slots.filter((slot) => {
|
|
626
|
+
const terms = normalizeTerms(slot).filter(
|
|
627
|
+
(term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
|
|
628
|
+
);
|
|
629
|
+
if (terms.length === 0) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
if (terms.length === 1 && COMPARISON_GENERIC_SLOT_PREFIXES.has(terms[0]!)) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
return true;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return filtered.length >= 1 ? filtered : slots;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function deriveComparisonSideSlots(slots: string[]): string[] {
|
|
642
|
+
const specificSlots = slots.filter((slot) => {
|
|
643
|
+
const terms = normalizeTerms(slot).filter(
|
|
644
|
+
(term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
|
|
645
|
+
);
|
|
646
|
+
if (terms.length === 0) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
return !(terms.length <= 2 && COMPARISON_GENERIC_SLOT_PREFIXES.has(terms[0]!));
|
|
650
|
+
});
|
|
651
|
+
const usableSlots = specificSlots.length >= 2 ? specificSlots : slots;
|
|
652
|
+
return usableSlots.slice(0, 2);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function computeComparisonSideScore(slots: string[], candidateText: string): number {
|
|
656
|
+
if (slots.length < 2) {
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const overlaps = slots
|
|
661
|
+
.map((slot) => computeSlotCoverage([slot], candidateText).coverage)
|
|
662
|
+
.sort((left, right) => right - left);
|
|
663
|
+
const strongest = overlaps[0] ?? 0;
|
|
664
|
+
const second = overlaps[1] ?? 0;
|
|
665
|
+
const asymmetry = Math.abs(strongest - second);
|
|
666
|
+
|
|
667
|
+
return clamp01((0.6 * asymmetry) + (0.4 * strongest));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function computeComparisonSideAffiliation(slots: string[], candidateText: string): 0 | 1 | null {
|
|
671
|
+
const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
|
|
672
|
+
try {
|
|
673
|
+
if (slots.length < 2) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const leftCoverage = activeComparisonExperimentConfig?.disableDiscriminativeAffiliation
|
|
678
|
+
? computeSlotCoverage([slots[0]!], candidateText).coverage
|
|
679
|
+
: computeComparisonAffiliationCoverage(slots[0]!, slots[1]!, candidateText);
|
|
680
|
+
const rightCoverage = activeComparisonExperimentConfig?.disableDiscriminativeAffiliation
|
|
681
|
+
? computeSlotCoverage([slots[1]!], candidateText).coverage
|
|
682
|
+
: computeComparisonAffiliationCoverage(slots[1]!, slots[0]!, candidateText);
|
|
683
|
+
if (leftCoverage >= 0.5 && leftCoverage > rightCoverage) {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
if (rightCoverage >= 0.5 && rightCoverage > leftCoverage) {
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
} finally {
|
|
691
|
+
if (activeComparisonProfile) {
|
|
692
|
+
activeComparisonProfile.sideAffiliationMs += elapsedMs(start);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function computeComparisonAffiliationCoverage(slot: string, otherSlot: string, candidateText: string): number {
|
|
698
|
+
const slotTerms = normalizeTerms(slot).filter(
|
|
699
|
+
(term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
|
|
700
|
+
);
|
|
701
|
+
const otherTerms = new Set(normalizeTerms(otherSlot).filter(
|
|
702
|
+
(term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
|
|
703
|
+
));
|
|
704
|
+
const discriminativeTerms = slotTerms.filter((term) => (
|
|
705
|
+
!COMPARISON_GENERIC_AFFILIATION_TERMS.has(term) && !otherTerms.has(term)
|
|
706
|
+
));
|
|
707
|
+
if (discriminativeTerms.length === 0) {
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const candidateTerms = new Set(normalizeTerms(candidateText));
|
|
712
|
+
const matched = discriminativeTerms.filter((term) => candidateTerms.has(term)).length;
|
|
713
|
+
return matched / discriminativeTerms.length;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function computeComparisonSlotSpecificityMetrics(
|
|
717
|
+
slot: string,
|
|
718
|
+
candidateText: string,
|
|
719
|
+
otherSlot?: string,
|
|
720
|
+
): {
|
|
721
|
+
recall: number;
|
|
722
|
+
precision: number;
|
|
723
|
+
specificity: number;
|
|
724
|
+
positionWeightedRecall: number;
|
|
725
|
+
positionWeightedPrecision: number;
|
|
726
|
+
positionWeightedSpecificity: number;
|
|
727
|
+
firstPersonClauseCount: number;
|
|
728
|
+
prospectivePersonalVerbCount: number;
|
|
729
|
+
planningDensity: number;
|
|
730
|
+
pastness: number;
|
|
731
|
+
sideWitnessScore: number;
|
|
732
|
+
} {
|
|
733
|
+
const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
|
|
734
|
+
try {
|
|
735
|
+
const slotTerms = normalizeContentTerms(slot);
|
|
736
|
+
const candidateTerms = normalizeContentTerms(candidateText);
|
|
737
|
+
const candidateTermSequence = normalizeContentTermSequence(candidateText);
|
|
738
|
+
const firstPositions = buildFirstContentTermPositions(candidateTermSequence);
|
|
739
|
+
const matchedTerms = [...slotTerms].filter((term) => candidateTerms.has(term));
|
|
740
|
+
const matched = matchedTerms.length;
|
|
741
|
+
const positionWeightedMatched = matchedTerms.reduce((sum, term) => {
|
|
742
|
+
const position = firstPositions.get(term);
|
|
743
|
+
if (typeof position !== "number") {
|
|
744
|
+
return sum;
|
|
745
|
+
}
|
|
746
|
+
const earlyThreshold = candidateTermSequence.length * 0.3;
|
|
747
|
+
return sum + (position < earlyThreshold ? 1 : 0.5);
|
|
748
|
+
}, 0);
|
|
749
|
+
const recall = slotTerms.size > 0 ? matched / slotTerms.size : 0;
|
|
750
|
+
const precision = matched / Math.max(5, candidateTerms.size);
|
|
751
|
+
const useSpecificity = slotTerms.size >= 2;
|
|
752
|
+
const specificity = recall * precision;
|
|
753
|
+
const positionWeightedRecall = slotTerms.size > 0 ? positionWeightedMatched / slotTerms.size : 0;
|
|
754
|
+
const positionWeightedPrecision = positionWeightedMatched / Math.max(5, candidateTerms.size);
|
|
755
|
+
const positionWeightedSpecificity = positionWeightedRecall * positionWeightedPrecision;
|
|
756
|
+
const { firstPersonClauseCount, prospectivePersonalVerbCount, planningDensity, pastness } = computePastness(candidateText);
|
|
757
|
+
const otherSlotTerms = otherSlot ? normalizeContentTerms(otherSlot) : new Set<string>();
|
|
758
|
+
const otherMatched = otherSlotTerms.size > 0
|
|
759
|
+
? [...otherSlotTerms].filter((term) => candidateTerms.has(term)).length
|
|
760
|
+
: 0;
|
|
761
|
+
const otherSlotRecall = otherSlotTerms.size > 0 ? otherMatched / otherSlotTerms.size : 0;
|
|
762
|
+
const purity = clamp01(1 - otherSlotRecall);
|
|
763
|
+
const purityMultiplier = activeComparisonExperimentConfig?.disableContaminationPenalty
|
|
764
|
+
? 1
|
|
765
|
+
: 0.7 + (0.3 * purity);
|
|
766
|
+
const rawWitnessScore = useSpecificity
|
|
767
|
+
? (activeComparisonExperimentConfig?.disableWitnessPositionPastness
|
|
768
|
+
? specificity
|
|
769
|
+
: positionWeightedSpecificity * Math.max(0.6, pastness))
|
|
770
|
+
: recall;
|
|
771
|
+
return {
|
|
772
|
+
recall,
|
|
773
|
+
precision,
|
|
774
|
+
specificity,
|
|
775
|
+
positionWeightedRecall,
|
|
776
|
+
positionWeightedPrecision,
|
|
777
|
+
positionWeightedSpecificity,
|
|
778
|
+
firstPersonClauseCount,
|
|
779
|
+
prospectivePersonalVerbCount,
|
|
780
|
+
planningDensity,
|
|
781
|
+
pastness,
|
|
782
|
+
sideWitnessScore: clamp01(rawWitnessScore * purityMultiplier),
|
|
783
|
+
};
|
|
784
|
+
} finally {
|
|
785
|
+
if (activeComparisonProfile) {
|
|
786
|
+
activeComparisonProfile.specificityMs += elapsedMs(start);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function findBestComparisonCoveragePair(
|
|
792
|
+
leftCandidates: Array<{
|
|
793
|
+
item: SearchResult;
|
|
794
|
+
finalScore: number;
|
|
795
|
+
comparisonSideWitnessScore?: number;
|
|
796
|
+
tokenEstimate: number;
|
|
797
|
+
slotMatches: string[];
|
|
798
|
+
}>,
|
|
799
|
+
rightCandidates: Array<{
|
|
800
|
+
item: SearchResult;
|
|
801
|
+
finalScore: number;
|
|
802
|
+
comparisonSideWitnessScore?: number;
|
|
803
|
+
tokenEstimate: number;
|
|
804
|
+
slotMatches: string[];
|
|
805
|
+
}>,
|
|
806
|
+
selectionTokenBudget: number,
|
|
807
|
+
): Array<{
|
|
808
|
+
item: SearchResult;
|
|
809
|
+
finalScore: number;
|
|
810
|
+
comparisonSideWitnessScore?: number;
|
|
811
|
+
tokenEstimate: number;
|
|
812
|
+
slotMatches: string[];
|
|
813
|
+
}> | null {
|
|
814
|
+
const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
|
|
815
|
+
let bestPair: Array<{
|
|
816
|
+
item: SearchResult;
|
|
817
|
+
finalScore: number;
|
|
818
|
+
comparisonSideWitnessScore?: number;
|
|
819
|
+
tokenEstimate: number;
|
|
820
|
+
slotMatches: string[];
|
|
821
|
+
}> | null = null;
|
|
822
|
+
let bestPairScore = Number.NEGATIVE_INFINITY;
|
|
823
|
+
|
|
824
|
+
for (const left of leftCandidates) {
|
|
825
|
+
for (const right of rightCandidates) {
|
|
826
|
+
if (left.item.id === right.item.id) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const totalTokens = left.tokenEstimate + right.tokenEstimate;
|
|
830
|
+
if (totalTokens > selectionTokenBudget) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const leftWitness = activeComparisonExperimentConfig?.disablePairScoreOnWitness
|
|
834
|
+
? left.finalScore
|
|
835
|
+
: (left.comparisonSideWitnessScore ?? left.finalScore);
|
|
836
|
+
const rightWitness = activeComparisonExperimentConfig?.disablePairScoreOnWitness
|
|
837
|
+
? right.finalScore
|
|
838
|
+
: (right.comparisonSideWitnessScore ?? right.finalScore);
|
|
839
|
+
const pairScore = leftWitness + rightWitness;
|
|
840
|
+
const currentTokens = bestPair ? bestPair[0]!.tokenEstimate + bestPair[1]!.tokenEstimate : Number.POSITIVE_INFINITY;
|
|
841
|
+
if (pairScore > bestPairScore || (pairScore === bestPairScore && totalTokens < currentTokens)) {
|
|
842
|
+
bestPair = [left, right];
|
|
843
|
+
bestPairScore = pairScore;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (activeComparisonProfile) {
|
|
848
|
+
activeComparisonProfile.pairSelectionMs += elapsedMs(start);
|
|
849
|
+
}
|
|
850
|
+
return bestPair;
|
|
851
|
+
}
|
|
852
|
+
|
|
389
853
|
function buildTemporalRecoveryRationale(slotCoverage: number, anchorDensity: number, semanticScore: number): string {
|
|
390
854
|
if (slotCoverage >= 0.5 && anchorDensity >= 0.5) {
|
|
391
855
|
return "slot coverage and temporal anchors both supported this candidate";
|
|
@@ -408,13 +872,126 @@ function computeRecencyScore(item: SearchResult, now: number, recencyLambda: num
|
|
|
408
872
|
return Math.exp(-recencyLambda * ageSeconds);
|
|
409
873
|
}
|
|
410
874
|
|
|
875
|
+
function splitTemporalClauses(text: string): string[] {
|
|
876
|
+
return text
|
|
877
|
+
.split(/(?:\bafter\b|\bbefore\b|\bbetween\b|\bor\b|\band\b|\bthen\b|[?.!,;\n]+)/i)
|
|
878
|
+
.map((part) => part.trim())
|
|
879
|
+
.filter((part) => part.length > 0);
|
|
880
|
+
}
|
|
881
|
+
|
|
411
882
|
function normalizeTerms(text: string): string[] {
|
|
883
|
+
if (activeComparisonProfile) {
|
|
884
|
+
activeComparisonProfile.normalizeTermsCalls += 1;
|
|
885
|
+
}
|
|
412
886
|
return text
|
|
413
887
|
.toLowerCase()
|
|
414
888
|
.split(/[^a-z0-9_]+/i)
|
|
415
889
|
.filter((term) => term.length > 0);
|
|
416
890
|
}
|
|
417
891
|
|
|
892
|
+
function normalizeContentTerms(text: string): Set<string> {
|
|
893
|
+
if (activeComparisonProfile) {
|
|
894
|
+
activeComparisonProfile.normalizeContentTermsCalls += 1;
|
|
895
|
+
}
|
|
896
|
+
return new Set(
|
|
897
|
+
normalizeContentTermSequence(text),
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function normalizeContentTermSequence(text: string): string[] {
|
|
902
|
+
if (activeComparisonProfile) {
|
|
903
|
+
activeComparisonProfile.normalizeContentTermsCalls += 1;
|
|
904
|
+
}
|
|
905
|
+
return normalizeTerms(text).filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function buildFirstContentTermPositions(terms: string[]): Map<string, number> {
|
|
909
|
+
const positions = new Map<string, number>();
|
|
910
|
+
for (let index = 0; index < terms.length; index += 1) {
|
|
911
|
+
const term = terms[index]!;
|
|
912
|
+
if (!positions.has(term)) {
|
|
913
|
+
positions.set(term, index);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return positions;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function computeComparisonFinalScore({
|
|
920
|
+
semanticScore,
|
|
921
|
+
recencyScore,
|
|
922
|
+
temporalAnchorDensity,
|
|
923
|
+
coverage,
|
|
924
|
+
comparisonSideWitnessScore,
|
|
925
|
+
temporalQueryActive,
|
|
926
|
+
}: {
|
|
927
|
+
semanticScore: number;
|
|
928
|
+
recencyScore: number;
|
|
929
|
+
temporalAnchorDensity: number;
|
|
930
|
+
coverage: number;
|
|
931
|
+
comparisonSideWitnessScore?: number;
|
|
932
|
+
temporalQueryActive: boolean;
|
|
933
|
+
}): number {
|
|
934
|
+
if (activeComparisonExperimentConfig?.disableComparisonBlend) {
|
|
935
|
+
return (0.40 * semanticScore) +
|
|
936
|
+
(0.25 * recencyScore) +
|
|
937
|
+
(0.20 * temporalAnchorDensity) +
|
|
938
|
+
(0.15 * coverage) +
|
|
939
|
+
(temporalQueryActive ? 0.05 : 0);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return (0.15 * semanticScore) +
|
|
943
|
+
(0.15 * recencyScore) +
|
|
944
|
+
(0.15 * coverage) +
|
|
945
|
+
(0.55 * (comparisonSideWitnessScore ?? 0));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function estimateTokensWithProfile(text: string): number {
|
|
949
|
+
if (activeComparisonProfile) {
|
|
950
|
+
activeComparisonProfile.estimateTokensCalls += 1;
|
|
951
|
+
}
|
|
952
|
+
return estimateTokens(text);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function recordSort(length: number): void {
|
|
956
|
+
if (!activeComparisonProfile) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
activeComparisonProfile.sortCalls += 1;
|
|
960
|
+
activeComparisonProfile.totalSortedLength += length;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function elapsedMs(start: bigint): number {
|
|
964
|
+
return Number(process.hrtime.bigint() - start) / 1_000_000;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function computePastness(text: string): {
|
|
968
|
+
firstPersonClauseCount: number;
|
|
969
|
+
prospectivePersonalVerbCount: number;
|
|
970
|
+
planningDensity: number;
|
|
971
|
+
pastness: number;
|
|
972
|
+
} {
|
|
973
|
+
const lower = text.toLowerCase();
|
|
974
|
+
const firstPersonClauseCount = Math.max(1, countPatternMatches(lower, COMPARISON_FIRST_PERSON_CLAUSE_PATTERNS));
|
|
975
|
+
const prospectivePersonalVerbCount = countPatternMatches(lower, COMPARISON_PROSPECTIVE_PERSONAL_PATTERNS);
|
|
976
|
+
const planningDensity = prospectivePersonalVerbCount / firstPersonClauseCount;
|
|
977
|
+
return {
|
|
978
|
+
firstPersonClauseCount,
|
|
979
|
+
prospectivePersonalVerbCount,
|
|
980
|
+
planningDensity,
|
|
981
|
+
pastness: clamp01(1 - planningDensity),
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function countPatternMatches(text: string, patterns: RegExp[]): number {
|
|
986
|
+
let count = 0;
|
|
987
|
+
for (const pattern of patterns) {
|
|
988
|
+
for (const _match of text.matchAll(pattern)) {
|
|
989
|
+
count += 1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return count;
|
|
993
|
+
}
|
|
994
|
+
|
|
418
995
|
function touchTemporalAnchorCache(cacheKey: string, value: number): void {
|
|
419
996
|
if (temporalAnchorCache.has(cacheKey)) {
|
|
420
997
|
temporalAnchorCache.delete(cacheKey);
|