fix: caller clock everywhere — value TTL, decay, qualitative decay, cache TTL
The principle: time is caller-provided (options.now / partialGraph.now); the wall clock is only the fallback for unpinned callers, never a hidden decision input. Remaining clock leaks: - getBlurredValue gained an optional now param threaded to _isValueExpired; ChainRule (2 sites), MultiHopRule (2 sites), RelationManager (3 sites) now pass the caller clock. Previously a pinned-clock caller's chain/ multi-hop value TTL used the WALL clock (wall in 2026, pinned T0 in 2001 -> values wrongly expired). - MultiHopRule's TTL gate used valueFilters.ttl || 24h instead of the valueManager's per-relation TTL (inconsistent with chain/comparator); now valueManager.getTTL is the authority, valueFilters.ttl the override. - QualitativeRelationalComparatorRule decay (_calculatePeriodsElapsed) and value timestamps used the wall clock, so qualitative possibility decay ignored the pinned clock; now threaded through _evaluateOperand. - ValueManager decay internals (getDecayedRelation, _calculateSeparated Decay, _calculateBlurredValue) accept a now param (background worker still passes none -> wall clock is correct there). - PartialGraphContext._addChallengeProof/_addRelation used Date.now() instead of the context's own this.now (the partial graph's time). - Arbiter gained an injectable clock (options.clock) driving unpinned cache-entry freshness in DecisionCache, RuleEvaluator, ChainRule, and RelationalComparatorRule; DecisionCache explicit clock still wins. - Collected-value timestamps in DirectRule and RelationalComparatorRule honor the caller clock. Pinned-clock chain probe: values fresh at T0, expired at T0+61s, with the wall clock in 2026. Rigor 251/251, full suite 853/791/0.
This commit is contained in:
@@ -36,7 +36,9 @@ export class DecisionCache {
|
||||
*/
|
||||
constructor(arbiter, options = {}) {
|
||||
this.arbiter = arbiter;
|
||||
this.clock = options.clock || (() => Date.now());
|
||||
// Precedence: an explicit cache-level clock wins; else the arbiter's
|
||||
// injected clock (options.clock on the Arbiter); else the wall clock.
|
||||
this.clock = options.clock || ((arbiter && typeof arbiter.clock === 'function') ? arbiter.clock.bind(arbiter) : (() => Date.now()));
|
||||
this._enabled = !!arbiter;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ export class RuleEvaluator {
|
||||
|
||||
if (canCacheRuleResult) {
|
||||
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
if (cached && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
this.arbiter.ruleResultCacheStats.hits++;
|
||||
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
|
||||
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
||||
@@ -159,9 +160,10 @@ export class RuleEvaluator {
|
||||
|
||||
_maybeCacheRuleResult(result, relation, cacheKey, enabled) {
|
||||
if (!enabled || !cacheKey || !relation) return result;
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.arbiter.ruleResultCache.set(cacheKey, {
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
timestamp: cacheNow
|
||||
});
|
||||
this.arbiter._cacheRuleResult(relation, cacheKey);
|
||||
return result;
|
||||
|
||||
@@ -433,7 +433,8 @@ export class ChainRule extends BaseRule {
|
||||
const key = this._getChainResultCacheKey(userId, objectId, steps);
|
||||
const entry = this.chainResultCache.get(key);
|
||||
|
||||
if (entry && Date.now() - entry.timestamp < this.cacheTTL) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
if (entry && cacheNow - entry.timestamp < this.cacheTTL) {
|
||||
return entry.result;
|
||||
}
|
||||
|
||||
@@ -451,9 +452,10 @@ export class ChainRule extends BaseRule {
|
||||
const key = this._getChainResultCacheKey(userId, objectId, steps);
|
||||
|
||||
// HyperbolicLRUCache handles eviction automatically based on frequency and recency
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.chainResultCache.set(key, {
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
timestamp: cacheNow
|
||||
});
|
||||
}
|
||||
|
||||
@@ -514,7 +516,8 @@ export class ChainRule extends BaseRule {
|
||||
return collectedValues;
|
||||
}
|
||||
|
||||
const blurred = this.arbiter.valueManager.getBlurredValue(relation);
|
||||
const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
|
||||
const blurred = this.arbiter.valueManager.getBlurredValue(relation, callerNow);
|
||||
|
||||
if (blurred.interval) {
|
||||
const sourceEntity = direction === 'in' ?
|
||||
@@ -590,7 +593,8 @@ export class ChainRule extends BaseRule {
|
||||
Arbiter.DEBUG && Arbiter.log('ChainRule: relationManager or valueManager is undefined');
|
||||
return collectedValues;
|
||||
}
|
||||
const blurred = this.arbiter.relationManager.valueManager.getBlurredValue(tempRelation);
|
||||
const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
|
||||
const blurred = this.arbiter.relationManager.valueManager.getBlurredValue(tempRelation, callerNow);
|
||||
|
||||
if (blurred.interval) {
|
||||
const collectedValue = this._createCollectedValue(
|
||||
|
||||
@@ -108,7 +108,8 @@ export class DirectRule extends BaseRule {
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(),
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at ||
|
||||
(options && options.now !== undefined && options.now !== null ? options.now : Date.now()),
|
||||
reliability: 1.0,
|
||||
source: directRel.source || 'persistent'
|
||||
}
|
||||
|
||||
@@ -370,21 +370,27 @@ export class MultiHopRule extends BaseRule {
|
||||
// Collect from direct edge if available
|
||||
if (step.edge && step.edge.value !== undefined && step.edge.value !== null) {
|
||||
if (this._passesValueFilters(step.edge.value, valueFilters)) {
|
||||
// Check TTL
|
||||
const ttl = valueFilters.ttl || 24 * 60 * 60 * 1000;
|
||||
const timestamp = step.edge.changed_last_at || step.edge.updated_last_at || Date.now();
|
||||
// Check TTL. The valueManager's per-relation TTL is the
|
||||
// authority (matching chain/comparator paths); valueFilters.ttl
|
||||
// is an explicit override. Timestamps and the age computation
|
||||
// use the caller-pinned clock, never the wall clock.
|
||||
const ttl = valueFilters.ttl || (valueManager && valueManager.getTTL
|
||||
? valueManager.getTTL(step.edge.rel)
|
||||
: 24 * 60 * 60 * 1000);
|
||||
const timestamp = step.edge.changed_last_at || step.edge.updated_last_at ||
|
||||
(evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now());
|
||||
|
||||
if (!OWAFusion.isWithinTTL(timestamp, ttl, evaluationNow)) {
|
||||
Arbiter.DEBUG && Arbiter.log('MultiHop skipping value due to TTL:', {
|
||||
value: step.edge.value,
|
||||
timestamp,
|
||||
ttl,
|
||||
age: Date.now() - timestamp
|
||||
age: evaluationNow - timestamp
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const blurred = valueManager.getBlurredValue(step.edge);
|
||||
const blurred = valueManager.getBlurredValue(step.edge, evaluationNow);
|
||||
|
||||
if (blurred.interval) {
|
||||
const collectedValue = this._createCollectedValue(
|
||||
@@ -447,7 +453,7 @@ export class MultiHopRule extends BaseRule {
|
||||
changed_last_at: contextValue.timestamp
|
||||
};
|
||||
|
||||
const blurred = valueManager.getBlurredValue(tempRelation);
|
||||
const blurred = valueManager.getBlurredValue(tempRelation, evaluationNow);
|
||||
|
||||
if (blurred.interval) {
|
||||
const collectedValue = this._createCollectedValue(
|
||||
|
||||
@@ -244,7 +244,7 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
}
|
||||
|
||||
// Extract blurred values with qualitative decay and blur
|
||||
const blurredValues = this._extractBlurredValues(adjustedValues, operandConfig, scale);
|
||||
const blurredValues = this._extractBlurredValues(adjustedValues, operandConfig, scale, options);
|
||||
|
||||
if (operandMeta) {
|
||||
operandMeta.blurredValues = blurredValues;
|
||||
@@ -299,7 +299,7 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
values.push({
|
||||
value: qualitativeValue,
|
||||
possibility: qualitativeValue,
|
||||
timestamp: Date.now(),
|
||||
timestamp: options && options.now !== undefined && options.now !== null ? options.now : Date.now(),
|
||||
relation: 'rule_result',
|
||||
meta: { source: 'rule_possibility' }
|
||||
});
|
||||
@@ -351,7 +351,7 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
* Extract blurred values with qualitative decay and blur
|
||||
* @private
|
||||
*/
|
||||
_extractBlurredValues(values, operandConfig, scale) {
|
||||
_extractBlurredValues(values, operandConfig, scale, options = null) {
|
||||
const {
|
||||
decaySteps = 1,
|
||||
decayPeriod = 'HOUR',
|
||||
@@ -366,10 +366,11 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
for (const valueObj of values) {
|
||||
const pointValue = valueObj.value;
|
||||
const initialPossibility = valueObj.possibility;
|
||||
const timestamp = valueObj.timestamp || Date.now();
|
||||
const timestamp = valueObj.timestamp ||
|
||||
(options && options.now !== undefined && options.now !== null ? options.now : Date.now());
|
||||
|
||||
// Calculate periods elapsed
|
||||
const periodsElapsed = this._calculatePeriodsElapsed(timestamp, decayPeriod);
|
||||
const periodsElapsed = this._calculatePeriodsElapsed(timestamp, decayPeriod, options);
|
||||
|
||||
// Calculate decayed possibility
|
||||
const decayedPossibility = this._calculateDecayedPossibility(
|
||||
@@ -415,8 +416,8 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
* Calculate periods elapsed since timestamp
|
||||
* @private
|
||||
*/
|
||||
_calculatePeriodsElapsed(timestamp, decayPeriod) {
|
||||
const now = Date.now();
|
||||
_calculatePeriodsElapsed(timestamp, decayPeriod, options = null) {
|
||||
const now = options && options.now !== undefined && options.now !== null ? options.now : Date.now();
|
||||
const elapsed = now - timestamp;
|
||||
|
||||
const periodMs = {
|
||||
@@ -533,7 +534,8 @@ export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
step: index
|
||||
},
|
||||
metadata: {
|
||||
timestamp: bv.timestamp || Date.now(),
|
||||
timestamp: bv.timestamp ||
|
||||
(options && options.now !== undefined && options.now !== null ? options.now : Date.now()),
|
||||
reliability: 1.0,
|
||||
originalValue: bv.originalValue,
|
||||
originalPossibility: bv.originalPossibility,
|
||||
|
||||
@@ -228,7 +228,8 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
: null;
|
||||
if (canCacheDerived) {
|
||||
const cached = this.arbiter.ruleResultCache.get(derivedCacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
if (cached && cacheNow - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
||||
const cachedResult = cached.result;
|
||||
let valueInterval = cachedResult.valueInterval;
|
||||
let operandPossibility = cachedResult.operandPossibility || 0;
|
||||
@@ -316,7 +317,8 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(),
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at ||
|
||||
(evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
|
||||
reliability: directRel.reliability || 1.0,
|
||||
source: directRel.source || 'persistent'
|
||||
}
|
||||
@@ -324,6 +326,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
}
|
||||
|
||||
if (canCacheDerived && derivedCacheKey) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.arbiter.ruleResultCache.set(derivedCacheKey, {
|
||||
result: {
|
||||
valueInterval,
|
||||
@@ -333,7 +336,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
source: operandSource,
|
||||
collectedValues: operandCollectedValues
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: cacheNow
|
||||
});
|
||||
this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
|
||||
if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
|
||||
@@ -397,7 +400,8 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
value: rel.value,
|
||||
possibility: rel.possibility !== undefined ? rel.possibility : 1.0,
|
||||
reliability: rel.reliability || 1.0,
|
||||
timestamp: rel.changed_last_at || rel.updated_last_at || Date.now(),
|
||||
timestamp: rel.changed_last_at || rel.updated_last_at ||
|
||||
(evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
|
||||
source: 'direct_relation',
|
||||
originalValue: rel.value
|
||||
});
|
||||
@@ -414,7 +418,8 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: rel.changed_last_at || rel.updated_last_at || Date.now(),
|
||||
timestamp: rel.changed_last_at || rel.updated_last_at ||
|
||||
(evaluationNow !== undefined && evaluationNow !== null ? evaluationNow : Date.now()),
|
||||
reliability: rel.reliability || 1.0,
|
||||
source: rel.source || 'persistent'
|
||||
}
|
||||
@@ -450,6 +455,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
}
|
||||
|
||||
if (canCacheDerived && derivedCacheKey) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.arbiter.ruleResultCache.set(derivedCacheKey, {
|
||||
result: {
|
||||
valueInterval,
|
||||
@@ -459,7 +465,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
source: operandSource,
|
||||
collectedValues: operandCollectedValues
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: cacheNow
|
||||
});
|
||||
this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
|
||||
if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
|
||||
@@ -566,6 +572,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
}
|
||||
|
||||
if (canCacheDerived && derivedCacheKey) {
|
||||
const cacheNow = this.arbiter.clock ? this.arbiter.clock() : Date.now();
|
||||
this.arbiter.ruleResultCache.set(derivedCacheKey, {
|
||||
result: {
|
||||
valueInterval,
|
||||
@@ -575,7 +582,7 @@ export class RelationalComparatorRule extends BaseRule {
|
||||
source: operandSource,
|
||||
collectedValues: operandCollectedValues
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: cacheNow
|
||||
});
|
||||
this.arbiter._cacheRuleResult(currentRelation, derivedCacheKey);
|
||||
if (operandEvalMeta) operandEvalMeta.steps.push({ step: 'DerivedValueCached', derivedCacheKey });
|
||||
|
||||
@@ -31,6 +31,12 @@ export class Arbiter {
|
||||
// is built only when the hook exists, so the default path is untouched.
|
||||
this._auditHook = typeof options.audit === 'function' ? options.audit : null;
|
||||
|
||||
// The engine's clock. The caller may inject a clock (options.clock);
|
||||
// the wall clock is the fallback for unpinned callers only. Per-check
|
||||
// time is always the caller's `{ now }` / partialGraph.now — this
|
||||
// clock only drives UNPINNED cache-entry freshness.
|
||||
this.clock = typeof options.clock === 'function' ? options.clock : (() => Date.now());
|
||||
|
||||
// Injectable cache factory (DI): defaults to the built-in SimpleLRUCache.
|
||||
// Pass options.cacheFactory to substitute another cache implementation.
|
||||
this.cacheFactory = options.cacheFactory || defaultCacheFactory;
|
||||
|
||||
@@ -126,7 +126,9 @@ export class PartialGraphContext {
|
||||
|
||||
_addChallengeProof(proof) {
|
||||
const subjectId = this._resolveNodeId(proof.subject);
|
||||
const now = Date.now();
|
||||
// The partial graph carries the caller's clock; fall back to the wall
|
||||
// clock only when the caller provided no time at all.
|
||||
const now = this.now !== null && this.now !== undefined ? this.now : Date.now();
|
||||
// 0 is a valid timestamp (epoch-issued / already-expired); `||` would
|
||||
// replace it with the wall clock or null, making an epoch-issued proof
|
||||
// the most recent one and an epoch-expired proof never expire.
|
||||
@@ -150,7 +152,7 @@ export class PartialGraphContext {
|
||||
const srcId = this._resolveNodeId(rel.src);
|
||||
const dstId = this._resolveNodeId(rel.dst);
|
||||
const relation = rel.relation;
|
||||
const now = Date.now();
|
||||
const now = this.now !== null && this.now !== undefined ? this.now : Date.now();
|
||||
const relationObj = {
|
||||
src: srcId,
|
||||
rel: relation,
|
||||
|
||||
@@ -327,7 +327,8 @@ export class RelationManager {
|
||||
this._ensureIndicesBuilt();
|
||||
const rel = this.arbiter.indices.getDirectRelation(srcId, relation, dstId);
|
||||
if (rel && rel.value !== undefined) {
|
||||
const blurred = this.arbiter.valueManager.getBlurredValue(rel);
|
||||
const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
|
||||
const blurred = this.arbiter.valueManager.getBlurredValue(rel, callerNow);
|
||||
const result = {
|
||||
pointValue: rel.value, // Original crisp value
|
||||
value: rel.value, // Backward compatibility
|
||||
@@ -543,7 +544,7 @@ export class RelationManager {
|
||||
* @param {number} epsilon - Optional epsilon for equality
|
||||
* @returns {number} Possibility (0-1) that comparison holds
|
||||
*/
|
||||
compareRelationValues(leftRelation, rightRelation, comparator, epsilon = null) {
|
||||
compareRelationValues(leftRelation, rightRelation, comparator, epsilon = null, options = null) {
|
||||
// Get actual relation objects if needed
|
||||
const leftRel = leftRelation.value !== undefined ? leftRelation :
|
||||
this.getDirectRelation(leftRelation.srcId, leftRelation.relation, leftRelation.dstId);
|
||||
@@ -554,8 +555,9 @@ export class RelationManager {
|
||||
return 0; // Cannot compare if either has no value
|
||||
}
|
||||
|
||||
const leftBlurred = this.arbiter.valueManager.getBlurredValue(leftRel);
|
||||
const rightBlurred = this.arbiter.valueManager.getBlurredValue(rightRel);
|
||||
const callerNow = options && options.now !== undefined && options.now !== null ? options.now : null;
|
||||
const leftBlurred = this.arbiter.valueManager.getBlurredValue(leftRel, callerNow);
|
||||
const rightBlurred = this.arbiter.valueManager.getBlurredValue(rightRel, callerNow);
|
||||
|
||||
if (!leftBlurred.interval || !rightBlurred.interval) {
|
||||
return 0; // Cannot compare null intervals
|
||||
|
||||
+15
-11
@@ -170,9 +170,11 @@ export class ValueManager {
|
||||
/**
|
||||
* Get or calculate the blurred value interval for a relation
|
||||
* @param {Object} relation - The relation object
|
||||
* @param {number|null} [now] - Caller-pinned clock; the wall clock is
|
||||
* only the fallback for unpinned callers.
|
||||
* @returns {Object} { interval: {min, max}, possibility: number, reliability: number }
|
||||
*/
|
||||
getBlurredValue(relation) {
|
||||
getBlurredValue(relation, now = null) {
|
||||
// If relation has no value, return null interval
|
||||
if (relation.value === undefined || relation.value === null) {
|
||||
return {
|
||||
@@ -183,7 +185,7 @@ export class ValueManager {
|
||||
}
|
||||
|
||||
// Check TTL first - if expired, return null interval
|
||||
if (this._isValueExpired(relation)) {
|
||||
if (this._isValueExpired(relation, now)) {
|
||||
return {
|
||||
interval: null,
|
||||
possibility: 0,
|
||||
@@ -204,7 +206,7 @@ export class ValueManager {
|
||||
* @param {Object} relation - The relation object
|
||||
* @returns {Object} { pointValue, blurredInterval, currentPossibility, originalPossibility, reliability }
|
||||
*/
|
||||
getDecayedRelation(relation) {
|
||||
getDecayedRelation(relation, now = null) {
|
||||
if (relation.value === undefined || relation.value === null) {
|
||||
return {
|
||||
pointValue: null,
|
||||
@@ -232,18 +234,19 @@ export class ValueManager {
|
||||
* Calculate separated decay for value blurring and possibility
|
||||
* @private
|
||||
*/
|
||||
_calculateSeparatedDecay(relation) {
|
||||
_calculateSeparatedDecay(relation, now = null) {
|
||||
const pointValue = relation.value;
|
||||
const initialPossibility = relation.possibility !== undefined ? relation.possibility : 1.0;
|
||||
const reliability = relation.reliability !== undefined ? relation.reliability : 1.0;
|
||||
const timestamp = relation.changed_last_at || relation.updated_last_at || Date.now();
|
||||
const timestamp = relation.changed_last_at || relation.updated_last_at ||
|
||||
(now !== null && now !== undefined ? now : Date.now());
|
||||
|
||||
// Get decay configuration
|
||||
const config = this._getRelationDecayConfig(relation);
|
||||
|
||||
// Calculate age
|
||||
const now = Date.now();
|
||||
const ageMs = Math.max(0, now - timestamp);
|
||||
const evalNow = now !== null && now !== undefined ? now : Date.now();
|
||||
const ageMs = Math.max(0, evalNow - timestamp);
|
||||
const periodMs = PERIOD_TO_MS[config.decayPeriod.toUpperCase()] || PERIOD_TO_MS.HOUR;
|
||||
const ageInPeriod = ageMs / periodMs;
|
||||
|
||||
@@ -636,18 +639,19 @@ export class ValueManager {
|
||||
* Calculate the blurred value interval for a relation
|
||||
* @private
|
||||
*/
|
||||
_calculateBlurredValue(relation) {
|
||||
_calculateBlurredValue(relation, now = null) {
|
||||
const pointValue = relation.value;
|
||||
const initialPossibility = relation.possibility !== undefined ? relation.possibility : 1.0;
|
||||
const reliability = relation.reliability !== undefined ? relation.reliability : 1.0;
|
||||
const timestamp = relation.changed_last_at || relation.updated_last_at || Date.now();
|
||||
const timestamp = relation.changed_last_at || relation.updated_last_at ||
|
||||
(now !== null && now !== undefined ? now : Date.now());
|
||||
|
||||
// Get decay configuration
|
||||
const config = this._getRelationDecayConfig(relation);
|
||||
|
||||
// Calculate age
|
||||
const now = Date.now();
|
||||
const ageMs = Math.max(0, now - timestamp); // Ensure non-negative
|
||||
const evalNow = now !== null && now !== undefined ? now : Date.now();
|
||||
const ageMs = Math.max(0, evalNow - timestamp); // Ensure non-negative
|
||||
const periodMs = PERIOD_TO_MS[config.decayPeriod.toUpperCase()] || PERIOD_TO_MS.HOUR;
|
||||
const ageInPeriod = ageMs / periodMs;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user