@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.
Files changed (74) 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 +48 -0
  6. package/dist/engines/decay/engine.d.ts.map +1 -0
  7. package/dist/engines/decay/engine.js +90 -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 +19 -0
  14. package/dist/engines/decay/strategy.d.ts.map +1 -0
  15. package/dist/engines/decay/strategy.js +284 -0
  16. package/dist/engines/decay/strategy.js.map +1 -0
  17. package/dist/engines/decay/types.d.ts +148 -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/engines/sentiment/engine.d.ts +25 -1
  42. package/dist/engines/sentiment/engine.d.ts.map +1 -1
  43. package/dist/engines/sentiment/engine.js +119 -0
  44. package/dist/engines/sentiment/engine.js.map +1 -1
  45. package/dist/engines/sentiment/index.d.ts +1 -1
  46. package/dist/engines/sentiment/index.d.ts.map +1 -1
  47. package/dist/engines/sentiment/index.js.map +1 -1
  48. package/dist/engines/sentiment/strategy.d.ts +2 -0
  49. package/dist/engines/sentiment/strategy.d.ts.map +1 -1
  50. package/dist/engines/sentiment/strategy.js +7 -8
  51. package/dist/engines/sentiment/strategy.js.map +1 -1
  52. package/dist/engines/sentiment/types.d.ts +9 -0
  53. package/dist/engines/sentiment/types.d.ts.map +1 -1
  54. package/dist/engines/sentiment/types.js.map +1 -1
  55. package/dist/index.d.ts +7 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +22 -6
  58. package/dist/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/engines/decay/compiler.ts +276 -0
  61. package/src/engines/decay/engine.ts +119 -0
  62. package/src/engines/decay/index.ts +42 -0
  63. package/src/engines/decay/strategy.ts +400 -0
  64. package/src/engines/decay/types.ts +221 -0
  65. package/src/engines/negotiation/compiler.ts +229 -0
  66. package/src/engines/negotiation/engine.ts +117 -0
  67. package/src/engines/negotiation/index.ts +42 -0
  68. package/src/engines/negotiation/strategy.ts +587 -0
  69. package/src/engines/negotiation/types.ts +244 -0
  70. package/src/engines/sentiment/engine.ts +157 -1
  71. package/src/engines/sentiment/index.ts +2 -1
  72. package/src/engines/sentiment/strategy.ts +12 -9
  73. package/src/engines/sentiment/types.ts +10 -0
  74. 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
+ };