@higher.archi/boe 1.0.27 → 1.0.29

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