fix: caller clock everywhere — value TTL, decay, qualitative decay, cache TTL
CI / test (push) Successful in 5m52s
CI / benchmark (push) Successful in 22s
CI / publish (push) Has been skipped

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:
John Dvorak
2026-08-02 17:50:24 -07:00
parent f530532e48
commit 440230b2c5
11 changed files with 84 additions and 46 deletions
@@ -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,