@higher.archi/boe 1.0.26 → 1.0.28
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/dist/engines/decay/compiler.d.ts +11 -0
- package/dist/engines/decay/compiler.d.ts.map +1 -0
- package/dist/engines/decay/compiler.js +216 -0
- package/dist/engines/decay/compiler.js.map +1 -0
- package/dist/engines/decay/engine.d.ts +48 -0
- package/dist/engines/decay/engine.d.ts.map +1 -0
- package/dist/engines/decay/engine.js +90 -0
- package/dist/engines/decay/engine.js.map +1 -0
- package/dist/engines/decay/index.d.ts +9 -0
- package/dist/engines/decay/index.d.ts.map +1 -0
- package/dist/engines/decay/index.js +21 -0
- package/dist/engines/decay/index.js.map +1 -0
- package/dist/engines/decay/strategy.d.ts +19 -0
- package/dist/engines/decay/strategy.d.ts.map +1 -0
- package/dist/engines/decay/strategy.js +284 -0
- package/dist/engines/decay/strategy.js.map +1 -0
- package/dist/engines/decay/types.d.ts +148 -0
- package/dist/engines/decay/types.d.ts.map +1 -0
- package/dist/engines/decay/types.js +39 -0
- package/dist/engines/decay/types.js.map +1 -0
- package/dist/engines/negotiation/compiler.d.ts +11 -0
- package/dist/engines/negotiation/compiler.d.ts.map +1 -0
- package/dist/engines/negotiation/compiler.js +177 -0
- package/dist/engines/negotiation/compiler.js.map +1 -0
- package/dist/engines/negotiation/engine.d.ts +46 -0
- package/dist/engines/negotiation/engine.d.ts.map +1 -0
- package/dist/engines/negotiation/engine.js +88 -0
- package/dist/engines/negotiation/engine.js.map +1 -0
- package/dist/engines/negotiation/index.d.ts +8 -0
- package/dist/engines/negotiation/index.d.ts.map +1 -0
- package/dist/engines/negotiation/index.js +17 -0
- package/dist/engines/negotiation/index.js.map +1 -0
- package/dist/engines/negotiation/strategy.d.ts +18 -0
- package/dist/engines/negotiation/strategy.d.ts.map +1 -0
- package/dist/engines/negotiation/strategy.js +439 -0
- package/dist/engines/negotiation/strategy.js.map +1 -0
- package/dist/engines/negotiation/types.d.ts +179 -0
- package/dist/engines/negotiation/types.d.ts.map +1 -0
- package/dist/engines/negotiation/types.js +10 -0
- package/dist/engines/negotiation/types.js.map +1 -0
- package/dist/engines/sentiment/engine.d.ts +25 -1
- package/dist/engines/sentiment/engine.d.ts.map +1 -1
- package/dist/engines/sentiment/engine.js +119 -0
- package/dist/engines/sentiment/engine.js.map +1 -1
- package/dist/engines/sentiment/index.d.ts +1 -1
- package/dist/engines/sentiment/index.d.ts.map +1 -1
- package/dist/engines/sentiment/index.js.map +1 -1
- package/dist/engines/sentiment/strategy.d.ts +2 -0
- package/dist/engines/sentiment/strategy.d.ts.map +1 -1
- package/dist/engines/sentiment/strategy.js +7 -8
- package/dist/engines/sentiment/strategy.js.map +1 -1
- package/dist/engines/sentiment/types.d.ts +9 -0
- package/dist/engines/sentiment/types.d.ts.map +1 -1
- package/dist/engines/sentiment/types.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engines/decay/compiler.ts +276 -0
- package/src/engines/decay/engine.ts +119 -0
- package/src/engines/decay/index.ts +42 -0
- package/src/engines/decay/strategy.ts +400 -0
- package/src/engines/decay/types.ts +221 -0
- package/src/engines/negotiation/compiler.ts +229 -0
- package/src/engines/negotiation/engine.ts +117 -0
- package/src/engines/negotiation/index.ts +42 -0
- package/src/engines/negotiation/strategy.ts +587 -0
- package/src/engines/negotiation/types.ts +244 -0
- package/src/engines/sentiment/engine.ts +157 -1
- package/src/engines/sentiment/index.ts +2 -1
- package/src/engines/sentiment/strategy.ts +12 -9
- package/src/engines/sentiment/types.ts +10 -0
- package/src/index.ts +70 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decay Engine Strategy
|
|
3
|
+
*
|
|
4
|
+
* Core execution logic for all decay strategies:
|
|
5
|
+
* - single-dimension: One temporal axis evaluated per entity
|
|
6
|
+
* - multi-dimension: Multiple temporal axes aggregated into a composite score
|
|
7
|
+
* - event-driven: Multi-dimension with re-engagement events that reset/boost/extend decay
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IWorkingMemory, Fact } from '../../core';
|
|
11
|
+
import { calculateDecayMultiplier } from '../../core/evaluation/decay';
|
|
12
|
+
import type { DecayConfig, DecayInfo } from '../../core/evaluation/decay';
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
CompiledDecayRuleSet,
|
|
16
|
+
CompiledSingleDimensionDecayRuleSet,
|
|
17
|
+
CompiledMultiDimensionDecayRuleSet,
|
|
18
|
+
CompiledEventDrivenDecayRuleSet,
|
|
19
|
+
CompiledDecayDimension,
|
|
20
|
+
DecayOptions,
|
|
21
|
+
DecayResult,
|
|
22
|
+
EntityDecayResult,
|
|
23
|
+
DimensionDecayResult,
|
|
24
|
+
ReEngagementEvent,
|
|
25
|
+
DecayAggregation,
|
|
26
|
+
UrgencyTier
|
|
27
|
+
} from './types';
|
|
28
|
+
import { resolveUrgencyTier } from './types';
|
|
29
|
+
|
|
30
|
+
const MS_PER_DAY = 86_400_000;
|
|
31
|
+
|
|
32
|
+
export class DecayExecutor {
|
|
33
|
+
run(
|
|
34
|
+
ruleSet: CompiledDecayRuleSet,
|
|
35
|
+
wm: IWorkingMemory,
|
|
36
|
+
options: DecayOptions = {}
|
|
37
|
+
): DecayResult {
|
|
38
|
+
const startTime = performance.now();
|
|
39
|
+
const asOf = options.asOf ?? new Date();
|
|
40
|
+
|
|
41
|
+
let entities: EntityDecayResult[];
|
|
42
|
+
|
|
43
|
+
switch (ruleSet.strategy) {
|
|
44
|
+
case 'single-dimension':
|
|
45
|
+
entities = this.runSingleDimension(ruleSet, wm, asOf, options);
|
|
46
|
+
break;
|
|
47
|
+
case 'multi-dimension':
|
|
48
|
+
entities = this.runMultiDimension(ruleSet, wm, asOf, options);
|
|
49
|
+
break;
|
|
50
|
+
case 'event-driven':
|
|
51
|
+
entities = this.runEventDriven(ruleSet, wm, asOf, options);
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown decay strategy: '${(ruleSet as any).strategy}'`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
58
|
+
|
|
59
|
+
// Build tier distribution
|
|
60
|
+
const tierDistribution: Record<UrgencyTier, number> = {
|
|
61
|
+
critical: 0,
|
|
62
|
+
high: 0,
|
|
63
|
+
medium: 0,
|
|
64
|
+
low: 0,
|
|
65
|
+
stale: 0
|
|
66
|
+
};
|
|
67
|
+
for (const entity of entities) {
|
|
68
|
+
tierDistribution[entity.urgencyTier]++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
entities,
|
|
73
|
+
totalEntities: entities.length,
|
|
74
|
+
strategy: ruleSet.strategy,
|
|
75
|
+
tierDistribution,
|
|
76
|
+
executionTimeMs
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ========================================
|
|
81
|
+
// Single-Dimension Strategy
|
|
82
|
+
// ========================================
|
|
83
|
+
|
|
84
|
+
private runSingleDimension(
|
|
85
|
+
ruleSet: CompiledSingleDimensionDecayRuleSet,
|
|
86
|
+
wm: IWorkingMemory,
|
|
87
|
+
asOf: Date,
|
|
88
|
+
options: DecayOptions
|
|
89
|
+
): EntityDecayResult[] {
|
|
90
|
+
const entityFacts = wm.getByType(ruleSet.entityType);
|
|
91
|
+
if (entityFacts.length === 0) return [];
|
|
92
|
+
|
|
93
|
+
const results: EntityDecayResult[] = [];
|
|
94
|
+
|
|
95
|
+
for (const fact of entityFacts) {
|
|
96
|
+
const entityId = resolveEntityId(fact, ruleSet.entityIdField);
|
|
97
|
+
const dimResult = computeDimensionDecay(fact, ruleSet.dimension, asOf);
|
|
98
|
+
|
|
99
|
+
const entity: EntityDecayResult = {
|
|
100
|
+
entityId,
|
|
101
|
+
compositeFreshness: dimResult.freshnessScore,
|
|
102
|
+
urgencyTier: resolveUrgencyTier(dimResult.freshnessScore, ruleSet.urgencyThresholds),
|
|
103
|
+
dimensions: [dimResult]
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (options.onEntity) {
|
|
107
|
+
options.onEntity(entity);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
results.push(entity);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ========================================
|
|
117
|
+
// Multi-Dimension Strategy
|
|
118
|
+
// ========================================
|
|
119
|
+
|
|
120
|
+
private runMultiDimension(
|
|
121
|
+
ruleSet: CompiledMultiDimensionDecayRuleSet,
|
|
122
|
+
wm: IWorkingMemory,
|
|
123
|
+
asOf: Date,
|
|
124
|
+
options: DecayOptions
|
|
125
|
+
): EntityDecayResult[] {
|
|
126
|
+
const entityFacts = wm.getByType(ruleSet.entityType);
|
|
127
|
+
if (entityFacts.length === 0) return [];
|
|
128
|
+
|
|
129
|
+
const results: EntityDecayResult[] = [];
|
|
130
|
+
|
|
131
|
+
for (const fact of entityFacts) {
|
|
132
|
+
const entityId = resolveEntityId(fact, ruleSet.entityIdField);
|
|
133
|
+
const dimResults = ruleSet.dimensions.map(dim =>
|
|
134
|
+
computeDimensionDecay(fact, dim, asOf)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const compositeFreshness = aggregateFreshness(
|
|
138
|
+
dimResults,
|
|
139
|
+
ruleSet.dimensions,
|
|
140
|
+
ruleSet.aggregation
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const entity: EntityDecayResult = {
|
|
144
|
+
entityId,
|
|
145
|
+
compositeFreshness,
|
|
146
|
+
urgencyTier: resolveUrgencyTier(compositeFreshness, ruleSet.urgencyThresholds),
|
|
147
|
+
dimensions: dimResults
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (options.onEntity) {
|
|
151
|
+
options.onEntity(entity);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
results.push(entity);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ========================================
|
|
161
|
+
// Event-Driven Strategy
|
|
162
|
+
// ========================================
|
|
163
|
+
|
|
164
|
+
private runEventDriven(
|
|
165
|
+
ruleSet: CompiledEventDrivenDecayRuleSet,
|
|
166
|
+
wm: IWorkingMemory,
|
|
167
|
+
asOf: Date,
|
|
168
|
+
options: DecayOptions
|
|
169
|
+
): EntityDecayResult[] {
|
|
170
|
+
const entityFacts = wm.getByType(ruleSet.entityType);
|
|
171
|
+
if (entityFacts.length === 0) return [];
|
|
172
|
+
|
|
173
|
+
const results: EntityDecayResult[] = [];
|
|
174
|
+
|
|
175
|
+
for (const fact of entityFacts) {
|
|
176
|
+
const entityId = resolveEntityId(fact, ruleSet.entityIdField);
|
|
177
|
+
const reEngagements: ReEngagementEvent[] = [];
|
|
178
|
+
|
|
179
|
+
// Build adjusted fact data by applying re-engagement rules
|
|
180
|
+
const adjustedData = { ...fact.data };
|
|
181
|
+
|
|
182
|
+
for (const rule of ruleSet.reEngagementRules) {
|
|
183
|
+
// Scan WM for events matching this rule's eventType that reference this entity
|
|
184
|
+
const events = wm.getByType(rule.eventType);
|
|
185
|
+
const matchingEvents = events.filter(e =>
|
|
186
|
+
e.data?.entityId === entityId || e.data?.targetId === entityId
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (matchingEvents.length === 0) continue;
|
|
190
|
+
|
|
191
|
+
// Use the most recent matching event
|
|
192
|
+
const latestEvent = matchingEvents.reduce((latest, e) => {
|
|
193
|
+
const latestTime = latest.data?.timestamp ? new Date(latest.data.timestamp).getTime() : 0;
|
|
194
|
+
const currentTime = e.data?.timestamp ? new Date(e.data.timestamp).getTime() : 0;
|
|
195
|
+
return currentTime > latestTime ? e : latest;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const eventTimestamp = latestEvent.data?.timestamp
|
|
199
|
+
? new Date(latestEvent.data.timestamp).toISOString()
|
|
200
|
+
: asOf.toISOString();
|
|
201
|
+
|
|
202
|
+
// Apply effect to each dimension's timestamp field
|
|
203
|
+
for (const dim of ruleSet.dimensions) {
|
|
204
|
+
const originalTimestamp = adjustedData[dim.timestampField];
|
|
205
|
+
if (!originalTimestamp) continue;
|
|
206
|
+
|
|
207
|
+
const originalDate = new Date(originalTimestamp);
|
|
208
|
+
const config: DecayConfig = {
|
|
209
|
+
curve: dim.curve,
|
|
210
|
+
timeUnit: dim.timeUnit,
|
|
211
|
+
halfLife: dim.halfLife,
|
|
212
|
+
maxAge: dim.maxAge,
|
|
213
|
+
threshold: dim.threshold,
|
|
214
|
+
floor: dim.floor
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const baseFreshness = calculateDecayMultiplier(originalDate, asOf, config).multiplier;
|
|
218
|
+
|
|
219
|
+
let adjustedFreshness = baseFreshness;
|
|
220
|
+
|
|
221
|
+
switch (rule.effect) {
|
|
222
|
+
case 'reset':
|
|
223
|
+
// Use the event timestamp instead of the original
|
|
224
|
+
adjustedData[dim.timestampField] = eventTimestamp;
|
|
225
|
+
adjustedFreshness = calculateDecayMultiplier(
|
|
226
|
+
new Date(eventTimestamp), asOf, config
|
|
227
|
+
).multiplier;
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'boost':
|
|
231
|
+
// Add boost amount, clamp to 1.0
|
|
232
|
+
adjustedFreshness = Math.min(1.0, baseFreshness + (rule.boostAmount ?? 0));
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case 'extend':
|
|
236
|
+
// Push original timestamp forward by extensionDays
|
|
237
|
+
const extendedDate = new Date(
|
|
238
|
+
originalDate.getTime() + (rule.extensionDays ?? 0) * MS_PER_DAY
|
|
239
|
+
);
|
|
240
|
+
adjustedData[dim.timestampField] = extendedDate.toISOString();
|
|
241
|
+
adjustedFreshness = calculateDecayMultiplier(
|
|
242
|
+
extendedDate, asOf, config
|
|
243
|
+
).multiplier;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
reEngagements.push({
|
|
248
|
+
ruleId: rule.id,
|
|
249
|
+
eventType: rule.eventType,
|
|
250
|
+
effect: rule.effect,
|
|
251
|
+
appliedAt: eventTimestamp,
|
|
252
|
+
freshnessBeforeBoost: baseFreshness,
|
|
253
|
+
freshnessAfterBoost: adjustedFreshness
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Compute dimension decay using adjusted data
|
|
259
|
+
const adjustedFact = { ...fact, data: adjustedData };
|
|
260
|
+
const dimResults = ruleSet.dimensions.map(dim =>
|
|
261
|
+
computeDimensionDecay(adjustedFact, dim, asOf)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// For boost effect, override freshness from re-engagement records
|
|
265
|
+
for (const reEng of reEngagements) {
|
|
266
|
+
if (reEng.effect === 'boost') {
|
|
267
|
+
// Find the matching dimension result and override its freshness
|
|
268
|
+
for (const dimResult of dimResults) {
|
|
269
|
+
// Apply the boosted freshness to the dimension
|
|
270
|
+
const originalFreshness = dimResult.freshnessScore;
|
|
271
|
+
if (originalFreshness === reEng.freshnessBeforeBoost) {
|
|
272
|
+
dimResult.freshnessScore = reEng.freshnessAfterBoost;
|
|
273
|
+
dimResult.fullyDecayed = dimResult.freshnessScore <= 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const compositeFreshness = aggregateFreshness(
|
|
280
|
+
dimResults,
|
|
281
|
+
ruleSet.dimensions,
|
|
282
|
+
ruleSet.aggregation
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const entity: EntityDecayResult = {
|
|
286
|
+
entityId,
|
|
287
|
+
compositeFreshness,
|
|
288
|
+
urgencyTier: resolveUrgencyTier(compositeFreshness, ruleSet.urgencyThresholds),
|
|
289
|
+
dimensions: dimResults,
|
|
290
|
+
reEngagements: reEngagements.length > 0 ? reEngagements : undefined
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (options.onEntity) {
|
|
294
|
+
options.onEntity(entity);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
results.push(entity);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ========================================
|
|
305
|
+
// Module-Level Helpers
|
|
306
|
+
// ========================================
|
|
307
|
+
|
|
308
|
+
function resolveEntityId(fact: Fact, entityIdField: string): string {
|
|
309
|
+
return fact.data?.[entityIdField] ?? fact.id;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function computeDimensionDecay(
|
|
313
|
+
fact: Fact,
|
|
314
|
+
dimension: CompiledDecayDimension,
|
|
315
|
+
asOf: Date
|
|
316
|
+
): DimensionDecayResult {
|
|
317
|
+
const timestampValue = fact.data?.[dimension.timestampField];
|
|
318
|
+
|
|
319
|
+
// If no timestamp found, treat as fully decayed
|
|
320
|
+
if (timestampValue === null || timestampValue === undefined) {
|
|
321
|
+
return {
|
|
322
|
+
dimensionId: dimension.id,
|
|
323
|
+
dimensionName: dimension.name,
|
|
324
|
+
freshnessScore: dimension.floor ?? 0,
|
|
325
|
+
ageInUnits: Infinity,
|
|
326
|
+
curve: dimension.curve,
|
|
327
|
+
timestamp: '',
|
|
328
|
+
fullyDecayed: true
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const dataTimestamp = new Date(timestampValue);
|
|
333
|
+
if (isNaN(dataTimestamp.getTime())) {
|
|
334
|
+
return {
|
|
335
|
+
dimensionId: dimension.id,
|
|
336
|
+
dimensionName: dimension.name,
|
|
337
|
+
freshnessScore: dimension.floor ?? 0,
|
|
338
|
+
ageInUnits: Infinity,
|
|
339
|
+
curve: dimension.curve,
|
|
340
|
+
timestamp: String(timestampValue),
|
|
341
|
+
fullyDecayed: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const config: DecayConfig = {
|
|
346
|
+
curve: dimension.curve,
|
|
347
|
+
timeUnit: dimension.timeUnit,
|
|
348
|
+
halfLife: dimension.halfLife,
|
|
349
|
+
maxAge: dimension.maxAge,
|
|
350
|
+
threshold: dimension.threshold,
|
|
351
|
+
floor: dimension.floor
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const decayInfo: DecayInfo = calculateDecayMultiplier(dataTimestamp, asOf, config);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
dimensionId: dimension.id,
|
|
358
|
+
dimensionName: dimension.name,
|
|
359
|
+
freshnessScore: decayInfo.multiplier,
|
|
360
|
+
ageInUnits: decayInfo.ageInUnits,
|
|
361
|
+
curve: decayInfo.curve,
|
|
362
|
+
timestamp: decayInfo.timestamp,
|
|
363
|
+
fullyDecayed: decayInfo.fullyDecayed
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function aggregateFreshness(
|
|
368
|
+
dimResults: DimensionDecayResult[],
|
|
369
|
+
dimensions: CompiledDecayDimension[],
|
|
370
|
+
aggregation: DecayAggregation
|
|
371
|
+
): number {
|
|
372
|
+
switch (aggregation) {
|
|
373
|
+
case 'weighted-average': {
|
|
374
|
+
let sum = 0;
|
|
375
|
+
for (let i = 0; i < dimResults.length; i++) {
|
|
376
|
+
sum += dimResults[i].freshnessScore * dimensions[i].weight;
|
|
377
|
+
}
|
|
378
|
+
return sum;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case 'minimum':
|
|
382
|
+
return Math.min(...dimResults.map(d => d.freshnessScore));
|
|
383
|
+
|
|
384
|
+
case 'geometric-mean': {
|
|
385
|
+
// Weighted geometric mean: exp(sum(weight * ln(freshness)))
|
|
386
|
+
let weightedLogSum = 0;
|
|
387
|
+
for (let i = 0; i < dimResults.length; i++) {
|
|
388
|
+
const freshness = Math.max(dimResults[i].freshnessScore, 1e-10); // avoid log(0)
|
|
389
|
+
weightedLogSum += dimensions[i].weight * Math.log(freshness);
|
|
390
|
+
}
|
|
391
|
+
return Math.exp(weightedLogSum);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
default:
|
|
395
|
+
throw new Error(`Unknown aggregation: '${aggregation}'`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Singleton instance */
|
|
400
|
+
export const decayStrategy = new DecayExecutor();
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decay Engine Types
|
|
3
|
+
*
|
|
4
|
+
* Temporal decay engine that computes freshness scores for entities based
|
|
5
|
+
* on how recently their data was updated. Supports single-dimension,
|
|
6
|
+
* multi-dimension, and event-driven decay strategies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DecayCurve, DecayTimeUnit } from '../../core/evaluation/decay';
|
|
10
|
+
import type { SemanticPriority } from '../utility/types';
|
|
11
|
+
|
|
12
|
+
// ========================================
|
|
13
|
+
// Semantic Types
|
|
14
|
+
// ========================================
|
|
15
|
+
|
|
16
|
+
/** Decay strategy to use */
|
|
17
|
+
export type DecayStrategy = 'single-dimension' | 'multi-dimension' | 'event-driven';
|
|
18
|
+
|
|
19
|
+
/** Urgency tier based on freshness score */
|
|
20
|
+
export type UrgencyTier = 'critical' | 'high' | 'medium' | 'low' | 'stale';
|
|
21
|
+
|
|
22
|
+
/** Effect applied when a re-engagement event is detected */
|
|
23
|
+
export type ReEngagementEffect = 'reset' | 'boost' | 'extend';
|
|
24
|
+
|
|
25
|
+
/** Aggregation method for combining multi-dimension freshness scores */
|
|
26
|
+
export type DecayAggregation = 'weighted-average' | 'minimum' | 'geometric-mean';
|
|
27
|
+
|
|
28
|
+
// ========================================
|
|
29
|
+
// Constants
|
|
30
|
+
// ========================================
|
|
31
|
+
|
|
32
|
+
/** Default urgency tier thresholds (freshness score boundaries) */
|
|
33
|
+
export const URGENCY_THRESHOLDS: Record<UrgencyTier, number> = {
|
|
34
|
+
'critical': 0.8,
|
|
35
|
+
'high': 0.6,
|
|
36
|
+
'medium': 0.4,
|
|
37
|
+
'low': 0.2,
|
|
38
|
+
'stale': 0
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ========================================
|
|
42
|
+
// Resolver Functions
|
|
43
|
+
// ========================================
|
|
44
|
+
|
|
45
|
+
/** Map a freshness score (0-1) to an urgency tier */
|
|
46
|
+
export function resolveUrgencyTier(
|
|
47
|
+
freshness: number,
|
|
48
|
+
thresholds?: Partial<Record<UrgencyTier, number>>
|
|
49
|
+
): UrgencyTier {
|
|
50
|
+
const t = { ...URGENCY_THRESHOLDS, ...thresholds };
|
|
51
|
+
if (freshness >= t.critical) return 'critical';
|
|
52
|
+
if (freshness >= t.high) return 'high';
|
|
53
|
+
if (freshness >= t.medium) return 'medium';
|
|
54
|
+
if (freshness >= t.low) return 'low';
|
|
55
|
+
return 'stale';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ========================================
|
|
59
|
+
// Dimension & Rule Types
|
|
60
|
+
// ========================================
|
|
61
|
+
|
|
62
|
+
/** A decay dimension -- a single temporal axis to evaluate */
|
|
63
|
+
export type DecayDimension = {
|
|
64
|
+
id: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
timestampField: string;
|
|
67
|
+
curve: DecayCurve;
|
|
68
|
+
timeUnit: DecayTimeUnit;
|
|
69
|
+
halfLife?: number;
|
|
70
|
+
maxAge?: number;
|
|
71
|
+
threshold?: number;
|
|
72
|
+
floor?: number;
|
|
73
|
+
weight: number | SemanticPriority;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Compiled dimension with resolved numeric weight */
|
|
77
|
+
export type CompiledDecayDimension = {
|
|
78
|
+
id: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
timestampField: string;
|
|
81
|
+
curve: DecayCurve;
|
|
82
|
+
timeUnit: DecayTimeUnit;
|
|
83
|
+
halfLife?: number;
|
|
84
|
+
maxAge?: number;
|
|
85
|
+
threshold?: number;
|
|
86
|
+
floor?: number;
|
|
87
|
+
weight: number; // normalized to sum to 1.0
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** A rule that adjusts decay when a re-engagement event is detected */
|
|
91
|
+
export type ReEngagementRule = {
|
|
92
|
+
id: string;
|
|
93
|
+
eventType: string;
|
|
94
|
+
effect: ReEngagementEffect;
|
|
95
|
+
boostAmount?: number;
|
|
96
|
+
extensionDays?: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ========================================
|
|
100
|
+
// Source RuleSet Types (Discriminated Union)
|
|
101
|
+
// ========================================
|
|
102
|
+
|
|
103
|
+
type DecayRuleSetBase = {
|
|
104
|
+
id: string;
|
|
105
|
+
name?: string;
|
|
106
|
+
mode: 'decay';
|
|
107
|
+
entityType: string;
|
|
108
|
+
entityIdField?: string;
|
|
109
|
+
urgencyThresholds?: Partial<Record<UrgencyTier, number>>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Single-dimension strategy: one temporal axis */
|
|
113
|
+
export type SingleDimensionDecayRuleSet = DecayRuleSetBase & {
|
|
114
|
+
strategy: 'single-dimension';
|
|
115
|
+
dimension: DecayDimension;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Multi-dimension strategy: multiple temporal axes aggregated together */
|
|
119
|
+
export type MultiDimensionDecayRuleSet = DecayRuleSetBase & {
|
|
120
|
+
strategy: 'multi-dimension';
|
|
121
|
+
dimensions: DecayDimension[];
|
|
122
|
+
aggregation?: DecayAggregation;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Event-driven strategy: multi-dimension with re-engagement rules */
|
|
126
|
+
export type EventDrivenDecayRuleSet = DecayRuleSetBase & {
|
|
127
|
+
strategy: 'event-driven';
|
|
128
|
+
dimensions: DecayDimension[];
|
|
129
|
+
aggregation?: DecayAggregation;
|
|
130
|
+
reEngagementRules: ReEngagementRule[];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type DecayRuleSet =
|
|
134
|
+
| SingleDimensionDecayRuleSet
|
|
135
|
+
| MultiDimensionDecayRuleSet
|
|
136
|
+
| EventDrivenDecayRuleSet;
|
|
137
|
+
|
|
138
|
+
// ========================================
|
|
139
|
+
// Compiled RuleSet Types
|
|
140
|
+
// ========================================
|
|
141
|
+
|
|
142
|
+
type CompiledDecayRuleSetBase = {
|
|
143
|
+
id: string;
|
|
144
|
+
name?: string;
|
|
145
|
+
mode: 'decay';
|
|
146
|
+
entityType: string;
|
|
147
|
+
entityIdField: string;
|
|
148
|
+
urgencyThresholds: Record<UrgencyTier, number>;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type CompiledSingleDimensionDecayRuleSet = CompiledDecayRuleSetBase & {
|
|
152
|
+
strategy: 'single-dimension';
|
|
153
|
+
dimension: CompiledDecayDimension;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type CompiledMultiDimensionDecayRuleSet = CompiledDecayRuleSetBase & {
|
|
157
|
+
strategy: 'multi-dimension';
|
|
158
|
+
dimensions: CompiledDecayDimension[];
|
|
159
|
+
aggregation: DecayAggregation;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type CompiledEventDrivenDecayRuleSet = CompiledDecayRuleSetBase & {
|
|
163
|
+
strategy: 'event-driven';
|
|
164
|
+
dimensions: CompiledDecayDimension[];
|
|
165
|
+
aggregation: DecayAggregation;
|
|
166
|
+
reEngagementRules: ReEngagementRule[];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export type CompiledDecayRuleSet =
|
|
170
|
+
| CompiledSingleDimensionDecayRuleSet
|
|
171
|
+
| CompiledMultiDimensionDecayRuleSet
|
|
172
|
+
| CompiledEventDrivenDecayRuleSet;
|
|
173
|
+
|
|
174
|
+
// ========================================
|
|
175
|
+
// Runtime Types
|
|
176
|
+
// ========================================
|
|
177
|
+
|
|
178
|
+
/** Per-dimension decay result */
|
|
179
|
+
export type DimensionDecayResult = {
|
|
180
|
+
dimensionId: string;
|
|
181
|
+
dimensionName?: string;
|
|
182
|
+
freshnessScore: number;
|
|
183
|
+
ageInUnits: number;
|
|
184
|
+
curve: DecayCurve;
|
|
185
|
+
timestamp: string;
|
|
186
|
+
fullyDecayed: boolean;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Record of a re-engagement event that was applied */
|
|
190
|
+
export type ReEngagementEvent = {
|
|
191
|
+
ruleId: string;
|
|
192
|
+
eventType: string;
|
|
193
|
+
effect: ReEngagementEffect;
|
|
194
|
+
appliedAt: string;
|
|
195
|
+
freshnessBeforeBoost: number;
|
|
196
|
+
freshnessAfterBoost: number;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** Per-entity decay result */
|
|
200
|
+
export type EntityDecayResult = {
|
|
201
|
+
entityId: string;
|
|
202
|
+
compositeFreshness: number;
|
|
203
|
+
urgencyTier: UrgencyTier;
|
|
204
|
+
dimensions: DimensionDecayResult[];
|
|
205
|
+
reEngagements?: ReEngagementEvent[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Full decay execution result */
|
|
209
|
+
export type DecayResult = {
|
|
210
|
+
entities: EntityDecayResult[];
|
|
211
|
+
totalEntities: number;
|
|
212
|
+
strategy: DecayStrategy;
|
|
213
|
+
tierDistribution: Record<UrgencyTier, number>;
|
|
214
|
+
executionTimeMs: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** Runtime options for decay execution */
|
|
218
|
+
export type DecayOptions = {
|
|
219
|
+
asOf?: Date;
|
|
220
|
+
onEntity?: (entity: EntityDecayResult) => void;
|
|
221
|
+
};
|