@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.
- 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/engines/bayesian/compiler.d.ts.map +1 -1
- package/dist/engines/bayesian/compiler.js +14 -2
- 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 +41 -4
- 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/defeasible/compiler.d.ts.map +1 -1
- package/dist/engines/defeasible/compiler.js +16 -2
- 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/scoring/compiler.d.ts.map +1 -1
- package/dist/engines/scoring/compiler.js +17 -4
- 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 +82 -2
- package/dist/engines/scoring/strategy.js.map +1 -1
- package/dist/engines/scoring/types.d.ts +17 -0
- package/dist/engines/scoring/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/evaluation/decay.ts +165 -0
- package/src/core/evaluation/index.ts +13 -0
- package/src/engines/bayesian/compiler.ts +15 -2
- package/src/engines/bayesian/strategy.ts +54 -4
- package/src/engines/bayesian/types.ts +17 -0
- package/src/engines/defeasible/compiler.ts +17 -2
- package/src/engines/defeasible/strategy.ts +62 -2
- package/src/engines/defeasible/types.ts +33 -0
- package/src/engines/scoring/compiler.ts +18 -4
- package/src/engines/scoring/strategy.ts +101 -3
- package/src/engines/scoring/types.ts +18 -0
- 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:
|
|
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:
|
|
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
|
|
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
|
// ========================================
|