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:
John Dvorak
2026-08-01 08:08:36 -07:00
parent 02d7a5dd75
commit f0dc14fb72
3 changed files with 80 additions and 7 deletions
+23 -2
View File
@@ -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);