@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.
- package/dist/core/errors.d.ts +48 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +63 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/evaluation/decay.d.ts +60 -0
- package/dist/core/evaluation/decay.d.ts.map +1 -0
- package/dist/core/evaluation/decay.js +111 -0
- package/dist/core/evaluation/decay.js.map +1 -0
- package/dist/core/evaluation/index.d.ts +2 -0
- package/dist/core/evaluation/index.d.ts.map +1 -1
- package/dist/core/evaluation/index.js +5 -1
- package/dist/core/evaluation/index.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/types/rule.d.ts +1 -0
- package/dist/core/types/rule.d.ts.map +1 -1
- package/dist/engines/backward/compiler.js +1 -1
- package/dist/engines/backward/compiler.js.map +1 -1
- package/dist/engines/bayesian/compiler.d.ts.map +1 -1
- package/dist/engines/bayesian/compiler.js +21 -9
- package/dist/engines/bayesian/compiler.js.map +1 -1
- package/dist/engines/bayesian/strategy.d.ts.map +1 -1
- package/dist/engines/bayesian/strategy.js +46 -9
- package/dist/engines/bayesian/strategy.js.map +1 -1
- package/dist/engines/bayesian/types.d.ts +16 -0
- package/dist/engines/bayesian/types.d.ts.map +1 -1
- package/dist/engines/bayesian/types.js.map +1 -1
- package/dist/engines/constraint/compiler.d.ts.map +1 -1
- package/dist/engines/constraint/compiler.js +10 -10
- package/dist/engines/constraint/compiler.js.map +1 -1
- package/dist/engines/defeasible/compiler.d.ts.map +1 -1
- package/dist/engines/defeasible/compiler.js +17 -3
- package/dist/engines/defeasible/compiler.js.map +1 -1
- package/dist/engines/defeasible/strategy.d.ts.map +1 -1
- package/dist/engines/defeasible/strategy.js +47 -2
- package/dist/engines/defeasible/strategy.js.map +1 -1
- package/dist/engines/defeasible/types.d.ts +29 -1
- package/dist/engines/defeasible/types.d.ts.map +1 -1
- package/dist/engines/defeasible/types.js.map +1 -1
- package/dist/engines/expert/compiler.d.ts.map +1 -1
- package/dist/engines/expert/compiler.js +6 -6
- package/dist/engines/expert/compiler.js.map +1 -1
- package/dist/engines/forward/compiler.d.ts.map +1 -1
- package/dist/engines/forward/compiler.js +4 -4
- package/dist/engines/forward/compiler.js.map +1 -1
- package/dist/engines/forward/strategy.d.ts.map +1 -1
- package/dist/engines/forward/strategy.js +4 -4
- package/dist/engines/forward/strategy.js.map +1 -1
- package/dist/engines/fuzzy/compiler.js +1 -1
- package/dist/engines/fuzzy/compiler.js.map +1 -1
- package/dist/engines/fuzzy/strategy.d.ts.map +1 -1
- package/dist/engines/fuzzy/strategy.js +8 -8
- package/dist/engines/fuzzy/strategy.js.map +1 -1
- package/dist/engines/monte-carlo/compiler.d.ts.map +1 -1
- package/dist/engines/monte-carlo/compiler.js +8 -8
- package/dist/engines/monte-carlo/compiler.js.map +1 -1
- package/dist/engines/monte-carlo/strategy.js +1 -1
- package/dist/engines/monte-carlo/strategy.js.map +1 -1
- package/dist/engines/pricing/compiler.d.ts.map +1 -1
- package/dist/engines/pricing/compiler.js +9 -9
- package/dist/engines/pricing/compiler.js.map +1 -1
- package/dist/engines/pricing/strategy.js +7 -6
- package/dist/engines/pricing/strategy.js.map +1 -1
- package/dist/engines/pricing/types.d.ts +2 -0
- package/dist/engines/pricing/types.d.ts.map +1 -1
- package/dist/engines/scoring/compiler.d.ts.map +1 -1
- package/dist/engines/scoring/compiler.js +24 -10
- package/dist/engines/scoring/compiler.js.map +1 -1
- package/dist/engines/scoring/strategy.d.ts.map +1 -1
- package/dist/engines/scoring/strategy.js +51 -2
- package/dist/engines/scoring/strategy.js.map +1 -1
- package/dist/engines/scoring/types.d.ts +16 -0
- package/dist/engines/scoring/types.d.ts.map +1 -1
- package/dist/engines/sequential/compiler.d.ts.map +1 -1
- package/dist/engines/sequential/compiler.js +4 -4
- package/dist/engines/sequential/compiler.js.map +1 -1
- package/dist/engines/sequential/strategy.js +1 -1
- package/dist/engines/sequential/strategy.js.map +1 -1
- package/dist/engines/state-machine/compiler.js +4 -4
- package/dist/engines/state-machine/compiler.js.map +1 -1
- package/dist/engines/state-machine/strategy.d.ts.map +1 -1
- package/dist/engines/state-machine/strategy.js +2 -1
- package/dist/engines/state-machine/strategy.js.map +1 -1
- package/dist/engines/utility/compiler.d.ts.map +1 -1
- package/dist/engines/utility/compiler.js +7 -7
- package/dist/engines/utility/compiler.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/errors.ts +69 -0
- package/src/core/evaluation/decay.ts +165 -0
- package/src/core/evaluation/index.ts +13 -0
- package/src/core/index.ts +4 -0
- package/src/core/types/rule.ts +1 -0
- package/src/engines/backward/compiler.ts +1 -1
- package/src/engines/bayesian/compiler.ts +24 -10
- package/src/engines/bayesian/strategy.ts +61 -10
- package/src/engines/bayesian/types.ts +17 -0
- package/src/engines/constraint/compiler.ts +12 -11
- package/src/engines/defeasible/compiler.ts +18 -3
- package/src/engines/defeasible/strategy.ts +62 -2
- package/src/engines/defeasible/types.ts +33 -0
- package/src/engines/expert/compiler.ts +8 -7
- package/src/engines/forward/compiler.ts +6 -5
- package/src/engines/forward/strategy.ts +6 -5
- package/src/engines/fuzzy/compiler.ts +1 -1
- package/src/engines/fuzzy/strategy.ts +9 -9
- package/src/engines/monte-carlo/compiler.ts +10 -9
- package/src/engines/monte-carlo/strategy.ts +2 -2
- package/src/engines/pricing/compiler.ts +11 -10
- package/src/engines/pricing/strategy.ts +7 -7
- package/src/engines/pricing/types.ts +2 -0
- package/src/engines/scoring/compiler.ts +27 -11
- package/src/engines/scoring/strategy.ts +67 -3
- package/src/engines/scoring/types.ts +17 -0
- package/src/engines/sequential/compiler.ts +6 -5
- package/src/engines/sequential/strategy.ts +2 -2
- package/src/engines/state-machine/compiler.ts +5 -5
- package/src/engines/state-machine/strategy.ts +3 -1
- package/src/engines/utility/compiler.ts +9 -8
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
161
|
+
for (const rule of activeRules) {
|
|
147
162
|
if (!rule.category) {
|
|
148
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
// ========================================
|