js-rigor: TTU operand values flow; modify honors changed_last_at override
Two issues found by the extended probe sweep: - A relational_comparator operand backed by a tuple_to_userset rule always denied: the TTU rule collected only the intermediate KEY, which the operand extraction skips as non-numeric, so no value was ever available. The TTU rule now emits a value-carrying collected entry when the tupleset edge carries a numeric value (entityKey = tuple src, relation = tupleset relation), keeping the bare intermediate key when there is no value. Comparator-with-TTU-operand now allows/denies on the tuple value through both persistent and partial contexts. - _modifyRelation ignored the changed_last_at override that the add path honors: value-changing modifies stamped fresh Date.now() regardless of the pin, so replay/restore tools pinning timestamps got different semantics via modify vs add. The override now applies to refresh events (value/reliability/possibility change) and is ignored for value-unchanged writes, preserving the TTL parity contract that identical replays never un-expire old values. Verified clean: intersection through partial, defeasible with logical when, challenge subject object/session with sessionKey, non-binary minAllowPossibility threshold, batch value updates, explain agreement under partial.
This commit is contained in:
@@ -175,6 +175,27 @@ export class TupleToUsersetRule extends BaseRule {
|
||||
let reasons = [];
|
||||
let intermediateEvaluationDetails = includeMeta ? [] : null;
|
||||
let processedCount = 0;
|
||||
|
||||
// Collected values carry the tupleset edge's value when it has one (the
|
||||
// intermediate key alone is not enough for downstream value consumers
|
||||
// like relational_comparator operands); otherwise the intermediate key
|
||||
// is collected for path-based value lookups.
|
||||
const buildCollectedValue = (tupleEdge, tupleSrcKey, intermediateKey) => {
|
||||
if (tupleEdge && typeof tupleEdge.value === 'number') {
|
||||
return this._createCollectedValue(
|
||||
tupleEdge.value,
|
||||
tupleEdge.possibility,
|
||||
[tupleSrcKey, intermediateKey],
|
||||
{ entityKey: tupleSrcKey, relation: rule.tuplesetRelation, step: 0 },
|
||||
{
|
||||
timestamp: tupleEdge.changed_last_at || tupleEdge.updated_last_at || Date.now(),
|
||||
reliability: tupleEdge.reliability || 1.0,
|
||||
source: tupleEdge.source || 'persistent'
|
||||
}
|
||||
);
|
||||
}
|
||||
return intermediateKey;
|
||||
};
|
||||
|
||||
// Process direct tuples with early exit optimization
|
||||
const computedRelationCache = new Map();
|
||||
@@ -258,7 +279,7 @@ export class TupleToUsersetRule extends BaseRule {
|
||||
computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
|
||||
}
|
||||
}),
|
||||
...(!useLightweightPaths && collectValues && { collectedValue: intermediateKey })
|
||||
...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(tupleEdge, resolveKey(tupleEdge.src, options), intermediateKey) })
|
||||
};
|
||||
allValidPaths.push(path);
|
||||
if (!bestPath || combinedPossibility > bestPath.possibility) {
|
||||
@@ -365,7 +386,7 @@ export class TupleToUsersetRule extends BaseRule {
|
||||
computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
|
||||
}
|
||||
}),
|
||||
...(!useLightweightPaths && collectValues && { collectedValue: intermediateKey })
|
||||
...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(t, entry.srcKey, intermediateKey) })
|
||||
};
|
||||
|
||||
allValidPaths.push(pathData);
|
||||
|
||||
@@ -600,11 +600,16 @@ export class RelationUpdates {
|
||||
oldRelation.value = newOptions?.value !== undefined ? newOptions.value : oldRelation.value;
|
||||
oldRelation.decayConfig = newOptions?.decayConfig !== undefined ? newOptions.decayConfig : oldRelation.decayConfig;
|
||||
oldRelation.updated_last_at = currentTime;
|
||||
oldRelation.changed_last_at = (newOptions?.value !== oldValue ||
|
||||
(newOptions?.reliability !== undefined && newOptions.reliability !== oldReliability) ||
|
||||
(newOptions?.possibility !== undefined && newOptions.possibility !== oldPossibility))
|
||||
? currentTime
|
||||
: oldRelation.changed_last_at;
|
||||
// Honor an explicit changed_last_at override like the add path does,
|
||||
// but only on writes that would refresh anyway: a write that does not
|
||||
// change the value (or its interpretation) keeps the old timestamp,
|
||||
// so replaying identical tuples never un-expires old values.
|
||||
const shouldRefresh = newOptions?.value !== oldValue ||
|
||||
(newOptions?.reliability !== undefined && newOptions.reliability !== oldReliability) ||
|
||||
(newOptions?.possibility !== undefined && newOptions.possibility !== oldPossibility);
|
||||
oldRelation.changed_last_at = shouldRefresh
|
||||
? (newOptions?.changed_last_at !== undefined ? newOptions.changed_last_at : currentTime)
|
||||
: oldRelation.changed_last_at;
|
||||
oldRelation.stateId = newStateId;
|
||||
this.manager._relationsChanged = true;
|
||||
this.manager._invalidateRelationCaches(srcId, relation, dstId);
|
||||
|
||||
Reference in New Issue
Block a user