@higher.archi/boe 1.0.6 → 1.0.8

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 (51) hide show
  1. package/dist/core/evaluation/decay.d.ts +60 -0
  2. package/dist/core/evaluation/decay.d.ts.map +1 -0
  3. package/dist/core/evaluation/decay.js +111 -0
  4. package/dist/core/evaluation/decay.js.map +1 -0
  5. package/dist/core/evaluation/index.d.ts +2 -0
  6. package/dist/core/evaluation/index.d.ts.map +1 -1
  7. package/dist/core/evaluation/index.js +5 -1
  8. package/dist/core/evaluation/index.js.map +1 -1
  9. package/dist/engines/bayesian/compiler.d.ts.map +1 -1
  10. package/dist/engines/bayesian/compiler.js +14 -2
  11. package/dist/engines/bayesian/compiler.js.map +1 -1
  12. package/dist/engines/bayesian/strategy.d.ts.map +1 -1
  13. package/dist/engines/bayesian/strategy.js +41 -4
  14. package/dist/engines/bayesian/strategy.js.map +1 -1
  15. package/dist/engines/bayesian/types.d.ts +16 -0
  16. package/dist/engines/bayesian/types.d.ts.map +1 -1
  17. package/dist/engines/bayesian/types.js.map +1 -1
  18. package/dist/engines/defeasible/compiler.d.ts.map +1 -1
  19. package/dist/engines/defeasible/compiler.js +16 -2
  20. package/dist/engines/defeasible/compiler.js.map +1 -1
  21. package/dist/engines/defeasible/strategy.d.ts.map +1 -1
  22. package/dist/engines/defeasible/strategy.js +47 -2
  23. package/dist/engines/defeasible/strategy.js.map +1 -1
  24. package/dist/engines/defeasible/types.d.ts +29 -1
  25. package/dist/engines/defeasible/types.d.ts.map +1 -1
  26. package/dist/engines/defeasible/types.js.map +1 -1
  27. package/dist/engines/scoring/compiler.d.ts.map +1 -1
  28. package/dist/engines/scoring/compiler.js +17 -4
  29. package/dist/engines/scoring/compiler.js.map +1 -1
  30. package/dist/engines/scoring/strategy.d.ts.map +1 -1
  31. package/dist/engines/scoring/strategy.js +82 -2
  32. package/dist/engines/scoring/strategy.js.map +1 -1
  33. package/dist/engines/scoring/types.d.ts +17 -0
  34. package/dist/engines/scoring/types.d.ts.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -1
  38. package/dist/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/core/evaluation/decay.ts +165 -0
  41. package/src/core/evaluation/index.ts +13 -0
  42. package/src/engines/bayesian/compiler.ts +15 -2
  43. package/src/engines/bayesian/strategy.ts +54 -4
  44. package/src/engines/bayesian/types.ts +17 -0
  45. package/src/engines/defeasible/compiler.ts +17 -2
  46. package/src/engines/defeasible/strategy.ts +62 -2
  47. package/src/engines/defeasible/types.ts +33 -0
  48. package/src/engines/scoring/compiler.ts +18 -4
  49. package/src/engines/scoring/strategy.ts +101 -3
  50. package/src/engines/scoring/types.ts +18 -0
  51. package/src/index.ts +12 -0
@@ -7,6 +7,12 @@
7
7
 
8
8
  import { match } from '../../core/matching';
9
9
  import { IWorkingMemory } from '../../core/memory';
10
+ import {
11
+ calculateDecayMultiplier,
12
+ resolveDecayTimestamp,
13
+ DecayConfig,
14
+ DecayInfo
15
+ } from '../../core/evaluation/decay';
10
16
 
11
17
  import {
12
18
  CompiledDefeasibleRuleSet,
@@ -101,17 +107,57 @@ export class DefeasibleStrategy {
101
107
  // Phase 1: Match all rules and collect fired rules
102
108
  const firedRules: FiredRule[] = [];
103
109
  const firedRuleIds: string[] = [];
110
+ const decayInfoMap: Record<string, DecayInfo> = {};
111
+ const decayMultipliersByRule: Record<string, number> = {};
112
+ const now = new Date();
104
113
 
105
114
  for (const rule of ruleSet.rules) {
106
115
  // Match inputs against working memory (includes when clause evaluation)
107
116
  const activations = match(rule, wm);
108
117
 
109
118
  for (const activation of activations) {
119
+ // Compute decay if configured
120
+ let effectiveCredibility = rule.credibility;
121
+ let credibilityBeforeDecay: number | undefined;
122
+ let ruleDecayInfo: DecayInfo | undefined;
123
+
124
+ const ruleDecay = rule.decay;
125
+ const engineDecay = config.decay;
126
+
127
+ if (ruleDecay || engineDecay) {
128
+ const tsRef = ruleDecay?.timestamp ?? (engineDecay?.timestamp as string | undefined);
129
+ const decayTarget = ruleDecay?.target ?? engineDecay?.target ?? 'credibility';
130
+
131
+ if (typeof tsRef === 'string') {
132
+ const dataTimestamp = resolveDecayTimestamp(tsRef, activation.facts);
133
+
134
+ if (dataTimestamp) {
135
+ const decayConfig: DecayConfig = {
136
+ ...(engineDecay?.config ?? { curve: 'exponential', timeUnit: 'days' }),
137
+ ...ruleDecay?.config
138
+ } as DecayConfig;
139
+
140
+ const info = calculateDecayMultiplier(dataTimestamp, now, decayConfig);
141
+ ruleDecayInfo = info;
142
+ decayInfoMap[rule.id] = info;
143
+ decayMultipliersByRule[rule.id] = info.multiplier;
144
+
145
+ // Apply decay to credibility if target includes it
146
+ if (decayTarget === 'credibility' || decayTarget === 'both') {
147
+ credibilityBeforeDecay = rule.credibility;
148
+ effectiveCredibility = rule.credibility * info.multiplier;
149
+ }
150
+ }
151
+ }
152
+ }
153
+
110
154
  // Rule fires
111
155
  firedRules.push({
112
156
  ruleId: rule.id,
113
157
  conclusion: rule.conclusion,
114
- credibility: rule.credibility,
158
+ credibility: effectiveCredibility,
159
+ credibilityBeforeDecay,
160
+ decayInfo: ruleDecayInfo,
115
161
  bindings: activation.facts
116
162
  });
117
163
 
@@ -166,9 +212,21 @@ export class DefeasibleStrategy {
166
212
  if (!defeatsMap.has(defeat.rule)) {
167
213
  defeatsMap.set(defeat.rule, []);
168
214
  }
215
+
216
+ // Apply decay to defeat strength if target includes it
217
+ let effectiveStrength = defeat.strength;
218
+ const ruleDecay = rule.decay;
219
+ const engineDecay = config.decay;
220
+ const decayTarget = ruleDecay?.target ?? engineDecay?.target ?? 'credibility';
221
+
222
+ if ((decayTarget === 'defeat-strength' || decayTarget === 'both') &&
223
+ decayMultipliersByRule[fired.ruleId] !== undefined) {
224
+ effectiveStrength = defeat.strength * decayMultipliersByRule[fired.ruleId];
225
+ }
226
+
169
227
  defeatsMap.get(defeat.rule)!.push({
170
228
  by: fired.ruleId,
171
- strength: defeat.strength
229
+ strength: effectiveStrength
172
230
  });
173
231
  }
174
232
  }
@@ -291,6 +349,7 @@ export class DefeasibleStrategy {
291
349
  };
292
350
 
293
351
  const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
352
+ const hasDecay = Object.keys(decayInfoMap).length > 0;
294
353
 
295
354
  return {
296
355
  conclusions,
@@ -300,6 +359,7 @@ export class DefeasibleStrategy {
300
359
  conflicts,
301
360
  fired: firedRuleIds,
302
361
  iterations: 1, // Current implementation is single-pass
362
+ ...(hasDecay ? { decayInfo: decayInfoMap } : {}),
303
363
  executionTimeMs,
304
364
  query
305
365
  };
@@ -20,9 +20,12 @@ import {
20
20
  CompiledBaseRule,
21
21
  CompiledBaseRuleSet,
22
22
  CompiledInputParameter,
23
+ Expression,
23
24
  BindingContext
24
25
  } from '../../core';
25
26
 
27
+ import { DecayConfig, DecayInfo } from '../../core/evaluation/decay';
28
+
26
29
  // ========================================
27
30
  // Rule Strength Types
28
31
  // ========================================
@@ -167,6 +170,15 @@ export type DefeasibleRule = BaseRule & {
167
170
  * Useful for audit trails and explainability.
168
171
  */
169
172
  explanation?: string;
173
+
174
+ /**
175
+ * Temporal decay configuration for this rule
176
+ */
177
+ decay?: {
178
+ timestamp: string | Expression;
179
+ config?: Partial<DecayConfig>;
180
+ target?: 'credibility' | 'defeat-strength' | 'both'; // default: 'credibility'
181
+ };
170
182
  };
171
183
 
172
184
  /**
@@ -216,6 +228,15 @@ export type DefeasibleConfig = {
216
228
  * Maximum iterations for computing defeat cycles (default: 100)
217
229
  */
218
230
  maxIterations?: number;
231
+
232
+ /**
233
+ * Engine-level temporal decay configuration
234
+ */
235
+ decay?: {
236
+ timestamp?: string | Expression;
237
+ config: DecayConfig;
238
+ target?: 'credibility' | 'defeat-strength' | 'both';
239
+ };
219
240
  };
220
241
 
221
242
  /**
@@ -263,6 +284,11 @@ export type CompiledDefeasibleRule = CompiledBaseRule & {
263
284
  conclusion: CompiledConclusion | null; // null for pure defeaters
264
285
  defeats: CompiledDefeat[]; // Normalized defeats
265
286
  specificity: number; // Number of conditions (for tie-breaking)
287
+ decay?: {
288
+ timestamp: string; // $-prefixed path
289
+ config?: Partial<DecayConfig>;
290
+ target?: 'credibility' | 'defeat-strength' | 'both';
291
+ };
266
292
  };
267
293
 
268
294
  /**
@@ -293,6 +319,8 @@ export type FiredRule = {
293
319
  ruleId: string;
294
320
  conclusion: CompiledConclusion | null;
295
321
  credibility: number;
322
+ credibilityBeforeDecay?: number; // original credibility before decay
323
+ decayInfo?: DecayInfo;
296
324
  bindings: BindingContext;
297
325
  };
298
326
 
@@ -385,6 +413,11 @@ export type DefeasibleResult = {
385
413
  */
386
414
  iterations: number;
387
415
 
416
+ /**
417
+ * Per-rule decay audit trail
418
+ */
419
+ decayInfo?: Record<string, DecayInfo>;
420
+
388
421
  /**
389
422
  * Processing time in milliseconds
390
423
  */
@@ -77,7 +77,18 @@ function compileInputParameter(input: any) {
77
77
  // ========================================
78
78
 
79
79
  function compileScoringAction(action: any): CompiledScoringAction {
80
- const { score, weight, normalize, reason, nullHandling } = action;
80
+ const { score, weight, normalize, reason, nullHandling, decay } = action;
81
+
82
+ // Compile decay timestamp if it's an expression
83
+ let compiledDecay: CompiledScoringAction['decay'] | undefined;
84
+ if (decay) {
85
+ compiledDecay = {
86
+ timestamp: Array.isArray(decay.timestamp)
87
+ ? compileCondition(decay.timestamp as Condition)
88
+ : decay.timestamp,
89
+ config: decay.config
90
+ };
91
+ }
81
92
 
82
93
  // Handle expression scores
83
94
  if (Array.isArray(score)) {
@@ -87,7 +98,8 @@ function compileScoringAction(action: any): CompiledScoringAction {
87
98
  weight,
88
99
  normalize,
89
100
  reason,
90
- nullHandling
101
+ nullHandling,
102
+ decay: compiledDecay
91
103
  };
92
104
  }
93
105
 
@@ -98,7 +110,8 @@ function compileScoringAction(action: any): CompiledScoringAction {
98
110
  weight,
99
111
  normalize,
100
112
  reason,
101
- nullHandling
113
+ nullHandling,
114
+ decay: compiledDecay
102
115
  };
103
116
  }
104
117
 
@@ -200,7 +213,8 @@ export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringR
200
213
  tiers: ruleSet.config?.tiers,
201
214
  categories,
202
215
  overrides,
203
- nullHandling: ruleSet.config?.nullHandling
216
+ nullHandling: ruleSet.config?.nullHandling,
217
+ decay: ruleSet.config?.decay
204
218
  };
205
219
 
206
220
  return {
@@ -10,6 +10,12 @@ import {
10
10
  evaluateCondition
11
11
  } from '../../core';
12
12
  import { IWorkingMemory } from '../../core/memory';
13
+ import {
14
+ calculateDecayMultiplier,
15
+ resolveDecayTimestamp,
16
+ DecayConfig,
17
+ DecayInfo
18
+ } from '../../core/evaluation/decay';
13
19
 
14
20
  import {
15
21
  CompiledScoringRuleSet,
@@ -19,9 +25,21 @@ import {
19
25
  ScoringTierMatch,
20
26
  OverrideResult,
21
27
  CategoryResult,
22
- TierDefinition
28
+ TierDefinition,
29
+ OutputBounds
23
30
  } from './types';
24
31
 
32
+ function resolveOutputBounds(bounds: OutputBounds): { min: number; max: number } {
33
+ if (typeof bounds === 'object') return bounds;
34
+ switch (bounds) {
35
+ case 'percentage': return { min: 0, max: 100 };
36
+ case 'points': return { min: 0, max: 1000 };
37
+ case 'rating': return { min: 0, max: 10 };
38
+ case 'stars': return { min: 0, max: 5 };
39
+ case 'normalized': return { min: 0, max: 1 };
40
+ }
41
+ }
42
+
25
43
  export class ScoringStrategy {
26
44
  run(
27
45
  ruleSet: CompiledScoringRuleSet,
@@ -37,8 +55,12 @@ export class ScoringStrategy {
37
55
  const weights: Record<string, number> = {};
38
56
  const fired: string[] = [];
39
57
  const rawScores: Record<string, number[]> = {}; // Track raw scores for adaptive
58
+ const decayInfoMap: Record<string, DecayInfo> = {};
59
+ const decayMultipliers: Record<string, number> = {};
40
60
 
41
61
  // Phase 1: Calculate base scores
62
+ const now = new Date();
63
+
42
64
  for (const rule of ruleSet.rules) {
43
65
  const activations = match(rule, wm);
44
66
 
@@ -62,6 +84,34 @@ export class ScoringStrategy {
62
84
  throw new Error(`Invalid score type: ${typeof ruleAction.score}`);
63
85
  }
64
86
 
87
+ // Apply temporal decay if configured
88
+ if (!decayMultipliers[rule.id]) {
89
+ const ruleDecay = ruleAction.decay;
90
+ const engineDecay = config.decay;
91
+
92
+ if (ruleDecay || engineDecay) {
93
+ // Resolve timestamp: rule-level overrides engine-level
94
+ const tsRef = ruleDecay?.timestamp ?? engineDecay?.timestamp;
95
+ let dataTimestamp: Date | null = null;
96
+
97
+ if (typeof tsRef === 'string') {
98
+ dataTimestamp = resolveDecayTimestamp(tsRef, activation.facts);
99
+ }
100
+
101
+ if (dataTimestamp) {
102
+ // Merge decay config: rule-level overrides engine-level
103
+ const decayConfig: DecayConfig = {
104
+ ...(engineDecay?.config ?? { curve: 'exponential', timeUnit: 'days' }),
105
+ ...ruleDecay?.config
106
+ } as DecayConfig;
107
+
108
+ const info = calculateDecayMultiplier(dataTimestamp, now, decayConfig);
109
+ decayInfoMap[rule.id] = info;
110
+ decayMultipliers[rule.id] = info.multiplier;
111
+ }
112
+ }
113
+ }
114
+
65
115
  // Store raw scores for adaptive strategy
66
116
  if (!rawScores[rule.id]) {
67
117
  rawScores[rule.id] = [];
@@ -78,10 +128,15 @@ export class ScoringStrategy {
78
128
  }
79
129
  }
80
130
 
81
- // Phase 2: Normalize and accumulate
131
+ // Phase 2: Normalize and accumulate (only for rules that fired)
82
132
  for (const rule of ruleSet.rules) {
83
133
  const ruleAction = rule.action as CompiledScoringAction;
84
134
  const ruleRawScores = rawScores[rule.id] || [];
135
+
136
+ // Only set contributions for rules that actually fired.
137
+ // Unfired rules are handled in Phase 2.5 (null handling).
138
+ if (ruleRawScores.length === 0) continue;
139
+
85
140
  let accumulatedScore = 0;
86
141
 
87
142
  for (const baseScore of ruleRawScores) {
@@ -122,6 +177,20 @@ export class ScoringStrategy {
122
177
  contributions[rule.id] = accumulatedScore;
123
178
  }
124
179
 
180
+ // Phase 2.25: Apply decay multipliers to contributions
181
+ const hasDecay = Object.keys(decayInfoMap).length > 0;
182
+ let contributionsBeforeDecay: Record<string, number> | undefined;
183
+
184
+ if (hasDecay) {
185
+ contributionsBeforeDecay = {};
186
+ for (const ruleId in contributions) {
187
+ contributionsBeforeDecay[ruleId] = contributions[ruleId];
188
+ if (decayMultipliers[ruleId] !== undefined) {
189
+ contributions[ruleId] *= decayMultipliers[ruleId];
190
+ }
191
+ }
192
+ }
193
+
125
194
  // Phase 2.5: Null handling — treat unfired 'zero' rules as fired with score 0
126
195
  for (const rule of ruleSet.rules) {
127
196
  if (!fired.includes(rule.id)) {
@@ -285,7 +354,8 @@ export class ScoringStrategy {
285
354
  const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0);
286
355
  if (totalWeight > 0) {
287
356
  for (const ruleId in contributions) {
288
- const normalizedWeight = weights[ruleId] / totalWeight;
357
+ const weight = weights[ruleId] ?? 0;
358
+ const normalizedWeight = weight / totalWeight;
289
359
  contributions[ruleId] = contributions[ruleId] * normalizedWeight;
290
360
  }
291
361
  }
@@ -420,6 +490,7 @@ export class ScoringStrategy {
420
490
  totalScore: 0,
421
491
  confidence,
422
492
  contributions: {},
493
+ contributionPercentages: {},
423
494
  fired: [],
424
495
  iterations: 1,
425
496
  override: {
@@ -465,17 +536,44 @@ export class ScoringStrategy {
465
536
  }
466
537
  }
467
538
 
539
+ // Phase 5: Apply outputBounds — clamp totalScore to configured range
540
+ if (config.outputBounds) {
541
+ const bounds = resolveOutputBounds(config.outputBounds);
542
+ totalScore = Math.max(bounds.min, Math.min(bounds.max, totalScore));
543
+ }
544
+
545
+ // Phase 6: Compute contribution percentages
546
+ const contributionTotal = Object.values(contributions).reduce((sum, v) => sum + v, 0);
547
+ const contributionPercentages: Record<string, number> = {};
548
+ if (contributionTotal !== 0) {
549
+ for (const ruleId in contributions) {
550
+ contributionPercentages[ruleId] = (contributions[ruleId] / contributionTotal) * 100;
551
+ }
552
+ }
553
+
468
554
  const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
469
555
 
556
+ // Compute totalScoreBeforeDecay if decay was applied
557
+ let totalScoreBeforeDecay: number | undefined;
558
+ if (hasDecay && contributionsBeforeDecay) {
559
+ totalScoreBeforeDecay = baseScore + Object.values(contributionsBeforeDecay).reduce((sum, s) => sum + s, 0);
560
+ }
561
+
470
562
  return {
471
563
  totalScore,
472
564
  confidence,
473
565
  contributions,
566
+ contributionPercentages,
474
567
  fired,
475
568
  iterations: 1,
476
569
  tier,
477
570
  categoryBreakdown,
478
571
  override: overrideResult,
572
+ ...(hasDecay ? {
573
+ totalScoreBeforeDecay,
574
+ contributionsBeforeDecay,
575
+ decayInfo: decayInfoMap
576
+ } : {}),
479
577
  executionTimeMs
480
578
  };
481
579
  }
@@ -13,6 +13,8 @@ import {
13
13
  BindingContext
14
14
  } from '../../core';
15
15
 
16
+ import { DecayConfig, DecayInfo } from '../../core/evaluation/decay';
17
+
16
18
  // ========================================
17
19
  // Scoring Method Types
18
20
  // ========================================
@@ -268,6 +270,10 @@ export type ScoringAction = {
268
270
  normalize?: { min: number; max: number }; // Optional normalization bounds
269
271
  reason?: string; // Optional human-readable explanation for the score
270
272
  nullHandling?: NullHandling; // How to handle if this signal is missing (default: 'exclude')
273
+ decay?: {
274
+ timestamp: string | Expression; // e.g. '$inspection.date'
275
+ config?: Partial<DecayConfig>; // overrides engine-level defaults
276
+ };
271
277
  };
272
278
 
273
279
  /**
@@ -300,6 +306,10 @@ export type ScoringConfig = {
300
306
  categories?: ScoringCategory[]; // Optional category grouping for two-level weight hierarchy
301
307
  overrides?: OverrideDefinition[]; // Optional post-scoring overrides evaluated in order (first match wins)
302
308
  nullHandling?: NullHandling; // Default null handling for all rules (default: 'exclude')
309
+ decay?: {
310
+ timestamp?: string | Expression; // default timestamp path for all rules
311
+ config: DecayConfig; // engine-level decay config
312
+ };
303
313
  };
304
314
 
305
315
  // ========================================
@@ -316,6 +326,10 @@ export type CompiledScoringAction = {
316
326
  normalize?: { min: number; max: number };
317
327
  reason?: string; // Preserved from source for reporting
318
328
  nullHandling?: NullHandling; // Preserved from source
329
+ decay?: {
330
+ timestamp: string | CompiledCondition; // string = $path, CompiledCondition = expression
331
+ config?: Partial<DecayConfig>;
332
+ };
319
333
  };
320
334
 
321
335
  /**
@@ -378,10 +392,14 @@ export type ScoringResult = {
378
392
  totalScore: number;
379
393
  confidence: number; // 0-1, signal coverage ratio (weighted if weights present)
380
394
  contributions: Record<string, number>; // Which rules contributed what scores
395
+ contributionPercentages: Record<string, number>; // Normalized percentages (0-100) per rule
381
396
  fired: string[]; // Rules that matched and contributed
382
397
  iterations: 1; // Scoring is always one-pass
383
398
  tier?: ScoringTierMatch; // Matched tier (if tiers configured)
384
399
  categoryBreakdown?: CategoryResult[]; // Per-category breakdown (if categories configured)
385
400
  override?: OverrideResult; // Override that fired (if any)
401
+ totalScoreBeforeDecay?: number; // undecayed total for comparison
402
+ contributionsBeforeDecay?: Record<string, number>; // undecayed per-rule contributions
403
+ decayInfo?: Record<string, DecayInfo>; // per-rule decay audit
386
404
  executionTimeMs: number; // Processing time in milliseconds
387
405
  };
package/src/index.ts CHANGED
@@ -375,6 +375,18 @@ export type {
375
375
 
376
376
  export { soundex, nysiis, caverphone2, cosineSimilarity } from './functions';
377
377
 
378
+ // Temporal Decay (re-exported from core for convenience)
379
+ export {
380
+ calculateDecayMultiplier,
381
+ resolveDecayTimestamp
382
+ } from './core/evaluation/decay';
383
+ export type {
384
+ DecayCurve,
385
+ DecayTimeUnit,
386
+ DecayConfig,
387
+ DecayInfo
388
+ } from './core/evaluation/decay';
389
+
378
390
  // ========================================
379
391
  // QFacts - Probabilistic Fact Hydration
380
392
  // ========================================