@higher.archi/boe 1.0.7 → 1.0.9

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 (125) hide show
  1. package/dist/core/errors.d.ts +48 -0
  2. package/dist/core/errors.d.ts.map +1 -0
  3. package/dist/core/errors.js +63 -0
  4. package/dist/core/errors.js.map +1 -0
  5. package/dist/core/evaluation/decay.d.ts +60 -0
  6. package/dist/core/evaluation/decay.d.ts.map +1 -0
  7. package/dist/core/evaluation/decay.js +111 -0
  8. package/dist/core/evaluation/decay.js.map +1 -0
  9. package/dist/core/evaluation/index.d.ts +2 -0
  10. package/dist/core/evaluation/index.d.ts.map +1 -1
  11. package/dist/core/evaluation/index.js +5 -1
  12. package/dist/core/evaluation/index.js.map +1 -1
  13. package/dist/core/index.d.ts +2 -0
  14. package/dist/core/index.d.ts.map +1 -1
  15. package/dist/core/index.js +6 -1
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/core/types/rule.d.ts +1 -0
  18. package/dist/core/types/rule.d.ts.map +1 -1
  19. package/dist/engines/backward/compiler.js +1 -1
  20. package/dist/engines/backward/compiler.js.map +1 -1
  21. package/dist/engines/bayesian/compiler.d.ts.map +1 -1
  22. package/dist/engines/bayesian/compiler.js +21 -9
  23. package/dist/engines/bayesian/compiler.js.map +1 -1
  24. package/dist/engines/bayesian/strategy.d.ts.map +1 -1
  25. package/dist/engines/bayesian/strategy.js +46 -9
  26. package/dist/engines/bayesian/strategy.js.map +1 -1
  27. package/dist/engines/bayesian/types.d.ts +16 -0
  28. package/dist/engines/bayesian/types.d.ts.map +1 -1
  29. package/dist/engines/bayesian/types.js.map +1 -1
  30. package/dist/engines/constraint/compiler.d.ts.map +1 -1
  31. package/dist/engines/constraint/compiler.js +10 -10
  32. package/dist/engines/constraint/compiler.js.map +1 -1
  33. package/dist/engines/defeasible/compiler.d.ts.map +1 -1
  34. package/dist/engines/defeasible/compiler.js +17 -3
  35. package/dist/engines/defeasible/compiler.js.map +1 -1
  36. package/dist/engines/defeasible/strategy.d.ts.map +1 -1
  37. package/dist/engines/defeasible/strategy.js +47 -2
  38. package/dist/engines/defeasible/strategy.js.map +1 -1
  39. package/dist/engines/defeasible/types.d.ts +29 -1
  40. package/dist/engines/defeasible/types.d.ts.map +1 -1
  41. package/dist/engines/defeasible/types.js.map +1 -1
  42. package/dist/engines/expert/compiler.d.ts.map +1 -1
  43. package/dist/engines/expert/compiler.js +6 -6
  44. package/dist/engines/expert/compiler.js.map +1 -1
  45. package/dist/engines/forward/compiler.d.ts.map +1 -1
  46. package/dist/engines/forward/compiler.js +4 -4
  47. package/dist/engines/forward/compiler.js.map +1 -1
  48. package/dist/engines/forward/strategy.d.ts.map +1 -1
  49. package/dist/engines/forward/strategy.js +4 -4
  50. package/dist/engines/forward/strategy.js.map +1 -1
  51. package/dist/engines/fuzzy/compiler.js +1 -1
  52. package/dist/engines/fuzzy/compiler.js.map +1 -1
  53. package/dist/engines/fuzzy/strategy.d.ts.map +1 -1
  54. package/dist/engines/fuzzy/strategy.js +8 -8
  55. package/dist/engines/fuzzy/strategy.js.map +1 -1
  56. package/dist/engines/monte-carlo/compiler.d.ts.map +1 -1
  57. package/dist/engines/monte-carlo/compiler.js +8 -8
  58. package/dist/engines/monte-carlo/compiler.js.map +1 -1
  59. package/dist/engines/monte-carlo/strategy.js +1 -1
  60. package/dist/engines/monte-carlo/strategy.js.map +1 -1
  61. package/dist/engines/pricing/compiler.d.ts.map +1 -1
  62. package/dist/engines/pricing/compiler.js +9 -9
  63. package/dist/engines/pricing/compiler.js.map +1 -1
  64. package/dist/engines/pricing/strategy.js +7 -6
  65. package/dist/engines/pricing/strategy.js.map +1 -1
  66. package/dist/engines/pricing/types.d.ts +2 -0
  67. package/dist/engines/pricing/types.d.ts.map +1 -1
  68. package/dist/engines/scoring/compiler.d.ts.map +1 -1
  69. package/dist/engines/scoring/compiler.js +24 -10
  70. package/dist/engines/scoring/compiler.js.map +1 -1
  71. package/dist/engines/scoring/strategy.d.ts.map +1 -1
  72. package/dist/engines/scoring/strategy.js +51 -2
  73. package/dist/engines/scoring/strategy.js.map +1 -1
  74. package/dist/engines/scoring/types.d.ts +16 -0
  75. package/dist/engines/scoring/types.d.ts.map +1 -1
  76. package/dist/engines/sequential/compiler.d.ts.map +1 -1
  77. package/dist/engines/sequential/compiler.js +4 -4
  78. package/dist/engines/sequential/compiler.js.map +1 -1
  79. package/dist/engines/sequential/strategy.js +1 -1
  80. package/dist/engines/sequential/strategy.js.map +1 -1
  81. package/dist/engines/state-machine/compiler.js +4 -4
  82. package/dist/engines/state-machine/compiler.js.map +1 -1
  83. package/dist/engines/state-machine/strategy.d.ts.map +1 -1
  84. package/dist/engines/state-machine/strategy.js +2 -1
  85. package/dist/engines/state-machine/strategy.js.map +1 -1
  86. package/dist/engines/utility/compiler.d.ts.map +1 -1
  87. package/dist/engines/utility/compiler.js +7 -7
  88. package/dist/engines/utility/compiler.js.map +1 -1
  89. package/dist/index.d.ts +4 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +10 -1
  92. package/dist/index.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/core/errors.ts +69 -0
  95. package/src/core/evaluation/decay.ts +165 -0
  96. package/src/core/evaluation/index.ts +13 -0
  97. package/src/core/index.ts +4 -0
  98. package/src/core/types/rule.ts +1 -0
  99. package/src/engines/backward/compiler.ts +1 -1
  100. package/src/engines/bayesian/compiler.ts +24 -10
  101. package/src/engines/bayesian/strategy.ts +61 -10
  102. package/src/engines/bayesian/types.ts +17 -0
  103. package/src/engines/constraint/compiler.ts +12 -11
  104. package/src/engines/defeasible/compiler.ts +18 -3
  105. package/src/engines/defeasible/strategy.ts +62 -2
  106. package/src/engines/defeasible/types.ts +33 -0
  107. package/src/engines/expert/compiler.ts +8 -7
  108. package/src/engines/forward/compiler.ts +6 -5
  109. package/src/engines/forward/strategy.ts +6 -5
  110. package/src/engines/fuzzy/compiler.ts +1 -1
  111. package/src/engines/fuzzy/strategy.ts +9 -9
  112. package/src/engines/monte-carlo/compiler.ts +10 -9
  113. package/src/engines/monte-carlo/strategy.ts +2 -2
  114. package/src/engines/pricing/compiler.ts +11 -10
  115. package/src/engines/pricing/strategy.ts +7 -7
  116. package/src/engines/pricing/types.ts +2 -0
  117. package/src/engines/scoring/compiler.ts +27 -11
  118. package/src/engines/scoring/strategy.ts +67 -3
  119. package/src/engines/scoring/types.ts +17 -0
  120. package/src/engines/sequential/compiler.ts +6 -5
  121. package/src/engines/sequential/strategy.ts +2 -2
  122. package/src/engines/state-machine/compiler.ts +5 -5
  123. package/src/engines/state-machine/strategy.ts +3 -1
  124. package/src/engines/utility/compiler.ts +9 -8
  125. package/src/index.ts +16 -0
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { IWorkingMemory } from '../../core/memory';
16
16
  import { Fact } from '../../core/types/fact';
17
- import { compileCondition, evaluateCondition, Condition } from '../../core';
17
+ import { compileCondition, evaluateCondition, Condition, RuntimeError } from '../../core';
18
18
 
19
19
  import {
20
20
  CompiledMonteCarloRuleSet,
@@ -137,7 +137,7 @@ function sampleVariable(
137
137
  case 'discrete':
138
138
  return sampleDiscrete(params.options!, params.weights!, random);
139
139
  default:
140
- throw new Error(`Unknown distribution: ${distribution}`);
140
+ throw new RuntimeError(`Unknown distribution: ${distribution}`);
141
141
  }
142
142
  }
143
143
 
@@ -9,7 +9,8 @@
9
9
 
10
10
  import {
11
11
  compileCondition,
12
- compileOperand
12
+ compileOperand,
13
+ CompilationError
13
14
  } from '../../core';
14
15
 
15
16
  import {
@@ -61,7 +62,7 @@ function validateScenarioValues(
61
62
  for (const [field, schema] of Object.entries(scenarioSchema)) {
62
63
  if (schema.required && values[field] === undefined && schema.default === undefined) {
63
64
  if (strict) {
64
- throw new Error(
65
+ throw new CompilationError(
65
66
  `Component '${component.sku}' missing required scenario value '${field}' for scenario '${scenarioId}'`
66
67
  );
67
68
  }
@@ -72,7 +73,7 @@ function validateScenarioValues(
72
73
  for (const [field, value] of Object.entries(values)) {
73
74
  const schema = scenarioSchema[field];
74
75
  if (!schema) {
75
- throw new Error(
76
+ throw new CompilationError(
76
77
  `Component '${component.sku}' has unknown scenario field '${field}' for scenario '${scenarioId}'`
77
78
  );
78
79
  }
@@ -94,19 +95,19 @@ function validateValue(
94
95
 
95
96
  const actualType = Array.isArray(value) ? 'array' : typeof value;
96
97
  if (actualType !== schema.type) {
97
- throw new Error(
98
+ throw new CompilationError(
98
99
  `Invalid type for '${path}': expected ${schema.type}, got ${actualType}`
99
100
  );
100
101
  }
101
102
 
102
103
  if (schema.type === 'number' && typeof value === 'number') {
103
104
  if (schema.min !== undefined && value < schema.min) {
104
- throw new Error(
105
+ throw new CompilationError(
105
106
  `Value for '${path}' is below minimum: ${value} < ${schema.min}`
106
107
  );
107
108
  }
108
109
  if (schema.max !== undefined && value > schema.max) {
109
- throw new Error(
110
+ throw new CompilationError(
110
111
  `Value for '${path}' exceeds maximum: ${value} > ${schema.max}`
111
112
  );
112
113
  }
@@ -114,7 +115,7 @@ function validateValue(
114
115
 
115
116
  if (schema.type === 'string' && schema.enum && typeof value === 'string') {
116
117
  if (!schema.enum.includes(value)) {
117
- throw new Error(
118
+ throw new CompilationError(
118
119
  `Invalid value for '${path}': '${value}' not in [${schema.enum.join(', ')}]`
119
120
  );
120
121
  }
@@ -161,7 +162,7 @@ function compilePricingMode(pricing: PricingMode): CompiledPricingMode {
161
162
  // Validate tiers are in order
162
163
  for (let i = 1; i < pricing.tiers.length; i++) {
163
164
  if (pricing.tiers[i].upTo <= pricing.tiers[i - 1].upTo) {
164
- throw new Error(
165
+ throw new CompilationError(
165
166
  `Graduated tiers must be in ascending order by 'upTo' value`
166
167
  );
167
168
  }
@@ -175,7 +176,7 @@ function compilePricingMode(pricing: PricingMode): CompiledPricingMode {
175
176
  // Validate brackets are in order
176
177
  for (let i = 1; i < pricing.brackets.length; i++) {
177
178
  if (pricing.brackets[i].minVolume <= pricing.brackets[i - 1].minVolume) {
178
- throw new Error(
179
+ throw new CompilationError(
179
180
  `Volume brackets must be in ascending order by 'minVolume' value`
180
181
  );
181
182
  }
@@ -362,7 +363,7 @@ export function compilePricingRuleSet(ruleSet: PricingRuleSet): CompiledPricingR
362
363
  currencyInfo,
363
364
  precision
364
365
  },
365
- rules: ruleSet.rules.map(rule =>
366
+ rules: ruleSet.rules.filter(r => !r.disabled).map(rule =>
366
367
  compileRule(rule, productTypes, scenarioIds, strict)
367
368
  )
368
369
  };
@@ -8,7 +8,7 @@
8
8
  * - Computes metrics for comparison
9
9
  */
10
10
 
11
- import { CompiledOperand, CompiledCondition } from '../../core';
11
+ import { CompiledOperand, CompiledCondition, RuntimeError } from '../../core';
12
12
 
13
13
  import {
14
14
  CompiledPricingRuleSet,
@@ -218,7 +218,7 @@ function executePricingMode(
218
218
  }
219
219
 
220
220
  case 'multi-meter':
221
- throw new Error('Multi-meter should be handled by executeMeter');
221
+ throw new RuntimeError('Multi-meter should be handled by executeMeter');
222
222
  }
223
223
  }
224
224
 
@@ -564,7 +564,7 @@ export class PricingStrategy {
564
564
  // Find the rule
565
565
  const rule = ruleSet.rules.find(r => r.id === ruleId);
566
566
  if (!rule) {
567
- throw new Error(`Rule not found: ${ruleId}`);
567
+ throw new RuntimeError(`Rule not found: ${ruleId}`);
568
568
  }
569
569
 
570
570
  // Determine scenario
@@ -573,12 +573,12 @@ export class PricingStrategy {
573
573
  || Object.keys(ruleSet.scenarios.definitions)[0];
574
574
 
575
575
  if (!scenarioId) {
576
- throw new Error('No scenario specified and no default scenario defined');
576
+ throw new RuntimeError('No scenario specified and no default scenario defined');
577
577
  }
578
578
 
579
579
  const scenarioDefinition = ruleSet.scenarios.definitions[scenarioId];
580
580
  if (!scenarioDefinition && Object.keys(ruleSet.scenarios.definitions).length > 0) {
581
- throw new Error(`Unknown scenario: ${scenarioId}`);
581
+ throw new RuntimeError(`Unknown scenario: ${scenarioId}`);
582
582
  }
583
583
 
584
584
  // Execute components
@@ -678,7 +678,7 @@ export class PricingStrategy {
678
678
  || Object.keys(ruleSet.scenarios.definitions);
679
679
 
680
680
  if (scenarios.length === 0) {
681
- throw new Error('No scenarios to calculate');
681
+ throw new RuntimeError('No scenarios to calculate');
682
682
  }
683
683
 
684
684
  // Calculate each scenario
@@ -687,7 +687,7 @@ export class PricingStrategy {
687
687
  for (const scenarioId of scenarios) {
688
688
  const scenarioDefinition = ruleSet.scenarios.definitions[scenarioId];
689
689
  if (!scenarioDefinition) {
690
- throw new Error(`Unknown scenario: ${scenarioId}`);
690
+ throw new RuntimeError(`Unknown scenario: ${scenarioId}`);
691
691
  }
692
692
 
693
693
  const pricingResult = this.calculate(ruleSet, ruleId, { scenario: scenarioId });
@@ -430,6 +430,8 @@ export type PricingRule = {
430
430
  name?: string;
431
431
  /** Description */
432
432
  description?: string;
433
+ /** Skip this rule during compilation (default: false) */
434
+ disabled?: boolean;
433
435
  /** Input bindings (for potential future filtering) */
434
436
  inputs?: PricingInput[];
435
437
  /** The pricing package */
@@ -5,7 +5,8 @@
5
5
  import {
6
6
  compileCondition,
7
7
  normalizeShape,
8
- Condition
8
+ Condition,
9
+ CompilationError
9
10
  } from '../../core';
10
11
 
11
12
  import {
@@ -77,7 +78,18 @@ function compileInputParameter(input: any) {
77
78
  // ========================================
78
79
 
79
80
  function compileScoringAction(action: any): CompiledScoringAction {
80
- const { score, weight, normalize, reason, nullHandling } = action;
81
+ const { score, weight, normalize, reason, nullHandling, decay } = action;
82
+
83
+ // Compile decay timestamp if it's an expression
84
+ let compiledDecay: CompiledScoringAction['decay'] | undefined;
85
+ if (decay) {
86
+ compiledDecay = {
87
+ timestamp: Array.isArray(decay.timestamp)
88
+ ? compileCondition(decay.timestamp as Condition)
89
+ : decay.timestamp,
90
+ config: decay.config
91
+ };
92
+ }
81
93
 
82
94
  // Handle expression scores
83
95
  if (Array.isArray(score)) {
@@ -87,7 +99,8 @@ function compileScoringAction(action: any): CompiledScoringAction {
87
99
  weight,
88
100
  normalize,
89
101
  reason,
90
- nullHandling
102
+ nullHandling,
103
+ decay: compiledDecay
91
104
  };
92
105
  }
93
106
 
@@ -98,7 +111,8 @@ function compileScoringAction(action: any): CompiledScoringAction {
98
111
  weight,
99
112
  normalize,
100
113
  reason,
101
- nullHandling
114
+ nullHandling,
115
+ decay: compiledDecay
102
116
  };
103
117
  }
104
118
 
@@ -136,6 +150,7 @@ function compileOverride(override: OverrideDefinition): CompiledOverrideDefiniti
136
150
  // ========================================
137
151
 
138
152
  export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringRuleSet {
153
+ const activeRules = ruleSet.rules.filter(r => !r.disabled);
139
154
  const categories = ruleSet.config?.categories;
140
155
 
141
156
  // Validate categories if defined
@@ -143,14 +158,14 @@ export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringR
143
158
  const categoryIds = new Set(categories.map(c => c.id));
144
159
 
145
160
  // Every rule must reference a valid category
146
- for (const rule of ruleSet.rules) {
161
+ for (const rule of activeRules) {
147
162
  if (!rule.category) {
148
- throw new Error(
163
+ throw new CompilationError(
149
164
  `Rule "${rule.id}" is missing a category. When categories are defined, every rule must have a category.`
150
165
  );
151
166
  }
152
167
  if (!categoryIds.has(rule.category)) {
153
- throw new Error(
168
+ throw new CompilationError(
154
169
  `Rule "${rule.id}" references unknown category "${rule.category}". Valid categories: ${[...categoryIds].join(', ')}`
155
170
  );
156
171
  }
@@ -176,12 +191,12 @@ export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringR
176
191
  // Validate tier effects reference valid tiers
177
192
  if (override.effect === 'force_tier' || override.effect === 'floor_tier' || override.effect === 'cap_tier') {
178
193
  if (!tierIds.size) {
179
- throw new Error(
194
+ throw new CompilationError(
180
195
  `Override "${override.id}" uses effect "${override.effect}" but no tiers are defined.`
181
196
  );
182
197
  }
183
198
  if (!tierIds.has(override.targetTier)) {
184
- throw new Error(
199
+ throw new CompilationError(
185
200
  `Override "${override.id}" references unknown tier "${override.targetTier}". Valid tiers: ${[...tierIds].join(', ')}`
186
201
  );
187
202
  }
@@ -200,7 +215,8 @@ export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringR
200
215
  tiers: ruleSet.config?.tiers,
201
216
  categories,
202
217
  overrides,
203
- nullHandling: ruleSet.config?.nullHandling
218
+ nullHandling: ruleSet.config?.nullHandling,
219
+ decay: ruleSet.config?.decay
204
220
  };
205
221
 
206
222
  return {
@@ -208,7 +224,7 @@ export function compileScoringRuleSet(ruleSet: ScoringRuleSet): CompiledScoringR
208
224
  name: ruleSet.name,
209
225
  mode: 'scoring',
210
226
  schema: ruleSet.shape ? normalizeShape(ruleSet.shape) : { type: 'object' },
211
- rules: ruleSet.rules.map((rule, index) => compileScoringRule(rule, index)),
227
+ rules: activeRules.map((rule, index) => compileScoringRule(rule, index)),
212
228
  config,
213
229
  overrides: compiledOverrides
214
230
  };
@@ -7,9 +7,16 @@
7
7
 
8
8
  import {
9
9
  match,
10
- evaluateCondition
10
+ evaluateCondition,
11
+ RuntimeError
11
12
  } from '../../core';
12
13
  import { IWorkingMemory } from '../../core/memory';
14
+ import {
15
+ calculateDecayMultiplier,
16
+ resolveDecayTimestamp,
17
+ DecayConfig,
18
+ DecayInfo
19
+ } from '../../core/evaluation/decay';
13
20
 
14
21
  import {
15
22
  CompiledScoringRuleSet,
@@ -49,8 +56,12 @@ export class ScoringStrategy {
49
56
  const weights: Record<string, number> = {};
50
57
  const fired: string[] = [];
51
58
  const rawScores: Record<string, number[]> = {}; // Track raw scores for adaptive
59
+ const decayInfoMap: Record<string, DecayInfo> = {};
60
+ const decayMultipliers: Record<string, number> = {};
52
61
 
53
62
  // Phase 1: Calculate base scores
63
+ const now = new Date();
64
+
54
65
  for (const rule of ruleSet.rules) {
55
66
  const activations = match(rule, wm);
56
67
 
@@ -64,14 +75,42 @@ export class ScoringStrategy {
64
75
  } else if (typeof ruleAction.score === 'string') {
65
76
  const scoreFunc = scoreFunctions[ruleAction.score];
66
77
  if (!scoreFunc) {
67
- throw new Error(`Score function not found: ${ruleAction.score}`);
78
+ throw new RuntimeError(`Score function not found: ${ruleAction.score}`);
68
79
  }
69
80
  baseScore = scoreFunc(activation.facts);
70
81
  } else if (typeof ruleAction.score === 'object' && 'op' in ruleAction.score) {
71
82
  const result = evaluateCondition(ruleAction.score as any, activation.facts);
72
83
  baseScore = typeof result === 'number' ? result : 0;
73
84
  } else {
74
- throw new Error(`Invalid score type: ${typeof ruleAction.score}`);
85
+ throw new RuntimeError(`Invalid score type: ${typeof ruleAction.score}`);
86
+ }
87
+
88
+ // Apply temporal decay if configured
89
+ if (!decayMultipliers[rule.id]) {
90
+ const ruleDecay = ruleAction.decay;
91
+ const engineDecay = config.decay;
92
+
93
+ if (ruleDecay || engineDecay) {
94
+ // Resolve timestamp: rule-level overrides engine-level
95
+ const tsRef = ruleDecay?.timestamp ?? engineDecay?.timestamp;
96
+ let dataTimestamp: Date | null = null;
97
+
98
+ if (typeof tsRef === 'string') {
99
+ dataTimestamp = resolveDecayTimestamp(tsRef, activation.facts);
100
+ }
101
+
102
+ if (dataTimestamp) {
103
+ // Merge decay config: rule-level overrides engine-level
104
+ const decayConfig: DecayConfig = {
105
+ ...(engineDecay?.config ?? { curve: 'exponential', timeUnit: 'days' }),
106
+ ...ruleDecay?.config
107
+ } as DecayConfig;
108
+
109
+ const info = calculateDecayMultiplier(dataTimestamp, now, decayConfig);
110
+ decayInfoMap[rule.id] = info;
111
+ decayMultipliers[rule.id] = info.multiplier;
112
+ }
113
+ }
75
114
  }
76
115
 
77
116
  // Store raw scores for adaptive strategy
@@ -139,6 +178,20 @@ export class ScoringStrategy {
139
178
  contributions[rule.id] = accumulatedScore;
140
179
  }
141
180
 
181
+ // Phase 2.25: Apply decay multipliers to contributions
182
+ const hasDecay = Object.keys(decayInfoMap).length > 0;
183
+ let contributionsBeforeDecay: Record<string, number> | undefined;
184
+
185
+ if (hasDecay) {
186
+ contributionsBeforeDecay = {};
187
+ for (const ruleId in contributions) {
188
+ contributionsBeforeDecay[ruleId] = contributions[ruleId];
189
+ if (decayMultipliers[ruleId] !== undefined) {
190
+ contributions[ruleId] *= decayMultipliers[ruleId];
191
+ }
192
+ }
193
+ }
194
+
142
195
  // Phase 2.5: Null handling — treat unfired 'zero' rules as fired with score 0
143
196
  for (const rule of ruleSet.rules) {
144
197
  if (!fired.includes(rule.id)) {
@@ -501,6 +554,12 @@ export class ScoringStrategy {
501
554
 
502
555
  const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
503
556
 
557
+ // Compute totalScoreBeforeDecay if decay was applied
558
+ let totalScoreBeforeDecay: number | undefined;
559
+ if (hasDecay && contributionsBeforeDecay) {
560
+ totalScoreBeforeDecay = baseScore + Object.values(contributionsBeforeDecay).reduce((sum, s) => sum + s, 0);
561
+ }
562
+
504
563
  return {
505
564
  totalScore,
506
565
  confidence,
@@ -511,6 +570,11 @@ export class ScoringStrategy {
511
570
  tier,
512
571
  categoryBreakdown,
513
572
  override: overrideResult,
573
+ ...(hasDecay ? {
574
+ totalScoreBeforeDecay,
575
+ contributionsBeforeDecay,
576
+ decayInfo: decayInfoMap
577
+ } : {}),
514
578
  executionTimeMs
515
579
  };
516
580
  }
@@ -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
  /**
@@ -384,5 +398,8 @@ export type ScoringResult = {
384
398
  tier?: ScoringTierMatch; // Matched tier (if tiers configured)
385
399
  categoryBreakdown?: CategoryResult[]; // Per-category breakdown (if categories configured)
386
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
387
404
  executionTimeMs: number; // Processing time in milliseconds
388
405
  };
@@ -8,7 +8,8 @@ import {
8
8
  parsePath,
9
9
  normalizeShape,
10
10
  Condition,
11
- PropertyRef
11
+ PropertyRef,
12
+ CompilationError
12
13
  } from '../../core';
13
14
 
14
15
  import {
@@ -77,7 +78,7 @@ function compileActionItem(item: ActionItem): CompiledActionItem {
77
78
 
78
79
  for (const [targetPath, value] of Object.entries(item.set)) {
79
80
  if (!isPropertyRef(targetPath)) {
80
- throw new Error(`Set target must be a property reference, got: ${targetPath}`);
81
+ throw new CompilationError(`Set target must be a property reference, got: ${targetPath}`);
81
82
  }
82
83
 
83
84
  if (isExpression(value)) {
@@ -101,7 +102,7 @@ function compileActionItem(item: ActionItem): CompiledActionItem {
101
102
  if ('retract' in item) {
102
103
  const ref = item.retract;
103
104
  if (!isPropertyRef(ref)) {
104
- throw new Error(`Retract must be a property reference, got: ${ref}`);
105
+ throw new CompilationError(`Retract must be a property reference, got: ${ref}`);
105
106
  }
106
107
  const paramName = ref.slice(1).split('.')[0];
107
108
  return { retract: paramName };
@@ -117,7 +118,7 @@ function compileActionItem(item: ActionItem): CompiledActionItem {
117
118
  return { halt: true };
118
119
  }
119
120
 
120
- throw new Error(`Unknown action item type: ${JSON.stringify(item)}`);
121
+ throw new CompilationError(`Unknown action item type: ${JSON.stringify(item)}`);
121
122
  }
122
123
 
123
124
  function normalizeThenValue(value: ThenValue): ActionItem[] {
@@ -195,7 +196,7 @@ export function compileSequentialRuleSet(ruleSet: SequentialRuleSet): CompiledSe
195
196
  name: ruleSet.name,
196
197
  mode: 'sequential',
197
198
  schema: ruleSet.shape ? normalizeShape(ruleSet.shape) : { type: 'object' },
198
- rules: ruleSet.rules.map((rule, index) => compileSequentialRule(rule, index)),
199
+ rules: ruleSet.rules.filter(r => !r.disabled).map((rule, index) => compileSequentialRule(rule, index)),
199
200
  config
200
201
  };
201
202
  }
@@ -5,7 +5,7 @@
5
5
  * and executes the first matching rule only.
6
6
  */
7
7
 
8
- import { match } from '../../core';
8
+ import { match, RuntimeError } from '../../core';
9
9
  import { IWorkingMemory } from '../../core/memory';
10
10
 
11
11
  import {
@@ -93,7 +93,7 @@ export class SequentialStrategy {
93
93
  if ('call' in item) {
94
94
  const handler = actions[item.call];
95
95
  if (!handler) {
96
- throw new Error(`Action not found: ${item.call}`);
96
+ throw new RuntimeError(`Action not found: ${item.call}`);
97
97
  }
98
98
  handler(activation.facts, wm);
99
99
  }
@@ -4,7 +4,7 @@
4
4
  * Compiles state machine definitions into an optimized format for execution.
5
5
  */
6
6
 
7
- import { compileCondition } from '../../core';
7
+ import { compileCondition, CompilationError } from '../../core';
8
8
 
9
9
  import {
10
10
  StateMachineDefinition,
@@ -70,7 +70,7 @@ function compileAction(action: StateMachineActionItem): CompiledStateMachineActi
70
70
  };
71
71
  }
72
72
 
73
- throw new Error(`Unknown action format: ${JSON.stringify(action)}`);
73
+ throw new CompilationError(`Unknown action format: ${JSON.stringify(action)}`);
74
74
  }
75
75
 
76
76
  function compileActions(actions?: StateMachineActionItem[]): CompiledStateMachineAction[] {
@@ -160,7 +160,7 @@ function buildStateMap(
160
160
  function validateStateMachine(definition: StateMachineDefinition): void {
161
161
  // Check initial state exists
162
162
  if (!definition.states[definition.initial]) {
163
- throw new Error(`Initial state '${definition.initial}' does not exist`);
163
+ throw new CompilationError(`Initial state '${definition.initial}' does not exist`);
164
164
  }
165
165
 
166
166
  // Validate all transition targets exist
@@ -201,7 +201,7 @@ function validateStateMachine(definition: StateMachineDefinition): void {
201
201
  const targetExists = possibleTargets.some(t => allStateNames.has(t));
202
202
 
203
203
  if (!targetExists && !allStateNames.has(target)) {
204
- throw new Error(
204
+ throw new CompilationError(
205
205
  `Invalid transition target '${transition.target}' from state '${fullName}'`
206
206
  );
207
207
  }
@@ -209,7 +209,7 @@ function validateStateMachine(definition: StateMachineDefinition): void {
209
209
 
210
210
  // Validate compound state has initial
211
211
  if (state.states && !state.initial) {
212
- throw new Error(`Compound state '${fullName}' must have an 'initial' property`);
212
+ throw new CompilationError(`Compound state '${fullName}' must have an 'initial' property`);
213
213
  }
214
214
 
215
215
  // Recursively validate nested states
@@ -5,6 +5,8 @@
5
5
  * Each instance tracks its own state, context, and history.
6
6
  */
7
7
 
8
+ import { RuntimeError } from '../../core';
9
+
8
10
  import {
9
11
  CompiledStateMachineDefinition,
10
12
  CompiledStateDefinition,
@@ -557,7 +559,7 @@ class StateMachineInstance implements IStateMachineInstance {
557
559
  );
558
560
  executed.push(`call:${action.handler}`);
559
561
  } else if (this.definition.config.strict) {
560
- throw new Error(`Action handler not found: ${action.handler}`);
562
+ throw new RuntimeError(`Action handler not found: ${action.handler}`);
561
563
  }
562
564
  }
563
565
  break;
@@ -5,7 +5,8 @@
5
5
  import {
6
6
  compileCondition,
7
7
  normalizeShape,
8
- Condition
8
+ Condition,
9
+ CompilationError
9
10
  } from '../../core';
10
11
 
11
12
  import {
@@ -88,7 +89,7 @@ function resolveWeight(weight: number | string): number {
88
89
  if (isSemanticPriority(weight)) {
89
90
  return SEMANTIC_PRIORITY_VALUES[weight];
90
91
  }
91
- throw new Error(`Invalid weight value: ${weight}. Must be a number (0-1) or SemanticPriority`);
92
+ throw new CompilationError(`Invalid weight value: ${weight}. Must be a number (0-1) or SemanticPriority`);
92
93
  }
93
94
 
94
95
  /**
@@ -100,7 +101,7 @@ function compileCriterion(criterion: UtilityCriterion): CompiledUtilityCriterion
100
101
 
101
102
  // For numeric weights, validate 0-1 range (semantic weights use different scale)
102
103
  if (typeof criterion.weight === 'number' && (numericWeight < 0 || numericWeight > 1)) {
103
- throw new Error(`Numeric criterion weight must be between 0 and 1, got ${numericWeight} for "${criterion.id}"`);
104
+ throw new CompilationError(`Numeric criterion weight must be between 0 and 1, got ${numericWeight} for "${criterion.id}"`);
104
105
  }
105
106
 
106
107
  return {
@@ -119,7 +120,7 @@ function normalizeWeights(criteria: CompiledUtilityCriterion[]): void {
119
120
  const totalWeight = criteria.reduce((sum, c) => sum + c.weight, 0);
120
121
 
121
122
  if (totalWeight === 0) {
122
- throw new Error('At least one criterion must have a non-zero weight');
123
+ throw new CompilationError('At least one criterion must have a non-zero weight');
123
124
  }
124
125
 
125
126
  for (const criterion of criteria) {
@@ -158,7 +159,7 @@ export function compileUtilityRule(rule: UtilityRule, index: number = 0): Compil
158
159
  export function compileUtilityRuleSet(ruleSet: UtilityRuleSet): CompiledUtilityRuleSet {
159
160
  // Validate criteria
160
161
  if (!ruleSet.criteria || ruleSet.criteria.length === 0) {
161
- throw new Error('UtilityRuleSet must have at least one criterion');
162
+ throw new CompilationError('UtilityRuleSet must have at least one criterion');
162
163
  }
163
164
 
164
165
  // Check if any criteria use semantic weights
@@ -176,7 +177,7 @@ export function compileUtilityRuleSet(ruleSet: UtilityRuleSet): CompiledUtilityR
176
177
  // Numeric weights: validate they sum to ~1.0, then normalize for precision
177
178
  const totalWeight = compiledCriteria.reduce((sum, c) => sum + c.weight, 0);
178
179
  if (Math.abs(totalWeight - 1.0) > 0.01) {
179
- throw new Error(`Criterion weights must sum to 1.0, got ${totalWeight.toFixed(4)}`);
180
+ throw new CompilationError(`Criterion weights must sum to 1.0, got ${totalWeight.toFixed(4)}`);
180
181
  }
181
182
  // Normalize for floating point precision
182
183
  normalizeWeights(compiledCriteria);
@@ -186,7 +187,7 @@ export function compileUtilityRuleSet(ruleSet: UtilityRuleSet): CompiledUtilityR
186
187
  const criterionIds = new Set(compiledCriteria.map(c => c.id));
187
188
  for (const rule of ruleSet.rules) {
188
189
  if (!criterionIds.has(rule.criterion)) {
189
- throw new Error(`Rule "${rule.id}" references unknown criterion "${rule.criterion}"`);
190
+ throw new CompilationError(`Rule "${rule.id}" references unknown criterion "${rule.criterion}"`);
190
191
  }
191
192
  }
192
193
 
@@ -202,7 +203,7 @@ export function compileUtilityRuleSet(ruleSet: UtilityRuleSet): CompiledUtilityR
202
203
  mode: 'utility',
203
204
  schema: ruleSet.shape ? normalizeShape(ruleSet.shape) : { type: 'object' },
204
205
  criteria: compiledCriteria,
205
- rules: ruleSet.rules.map((rule, index) => compileUtilityRule(rule, index)),
206
+ rules: ruleSet.rules.filter(r => !r.disabled).map((rule, index) => compileUtilityRule(rule, index)),
206
207
  config
207
208
  };
208
209
  }
package/src/index.ts CHANGED
@@ -375,6 +375,22 @@ export type {
375
375
 
376
376
  export { soundex, nysiis, caverphone2, cosineSimilarity } from './functions';
377
377
 
378
+ // Typed Errors (re-exported from core for convenience)
379
+ export { BOEError, CompilationError, RuntimeError } from './core/errors';
380
+ export type { ErrorContext } from './core/errors';
381
+
382
+ // Temporal Decay (re-exported from core for convenience)
383
+ export {
384
+ calculateDecayMultiplier,
385
+ resolveDecayTimestamp
386
+ } from './core/evaluation/decay';
387
+ export type {
388
+ DecayCurve,
389
+ DecayTimeUnit,
390
+ DecayConfig,
391
+ DecayInfo
392
+ } from './core/evaluation/decay';
393
+
378
394
  // ========================================
379
395
  // QFacts - Probabilistic Fact Hydration
380
396
  // ========================================