js-rigor: fix TTU computed-join possibility loss and logical cache staleness

Two real bugs found by the new rule-kind x partial-graph parity campaign:

- TupleToUsersetRule 'computed' join mode (computed side has fewer
  intermediates than the tupleset side) pushed path objects carrying
  combinedPossibility, but _buildFinalResult reads path.possibility —
  every valid TTU grant in that mode silently returned 0, in persistent
  and partial contexts alike.

- _collectRelationUsages only registered explicit type:'direct' children,
  so shorthand logical operands ({ relation: 'owner' } inside union/
  intersection/exclusion) left the dependency index empty: writes to a
  base relation never invalidated cached logical decisions, and a check
  performed before an add kept serving its stale result forever.

New campaign rule-kind-partial-parity.test.js pins the full kind x
persistent/partial matrix (direct, chain, multi_hop, TTU out/in/reverse,
parent, computed, defeasible, union, exclusion, comparator, challenge,
binary) plus seeded differential properties for TTU, comparator, and
exclusion; artifact persistence disabled to avoid disk bloat.
This commit is contained in:
John Dvorak
2026-07-31 17:09:46 -07:00
parent ad9aa22225
commit 0149926344
3 changed files with 510 additions and 4 deletions
+12 -3
View File
@@ -235,14 +235,23 @@ export class TupleToUsersetRule extends BaseRule {
if (combinedPossibility >= minPossibility) {
const path = {
possibility: combinedPossibility,
reliability: combinedReliability,
intermediateKey,
tuplesetPossibility: resolvePossibility(tupleEdge.possibility),
computedPossibility: res.possibility,
combinedPossibility,
combinedReliability
...(!useLightweightPaths && includeMeta && {
meta: {
intermediateKey,
pathType: 'direct_tuple',
tuplesetRelation: { relation: rule.tuplesetRelation, possibility: resolvePossibility(tupleEdge.possibility), reliability: resolveReliability(tupleEdge.reliability) },
computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
}
}),
...(!useLightweightPaths && collectValues && { collectedValue: intermediateKey })
};
allValidPaths.push(path);
if (!bestPath || combinedPossibility > bestPath.combinedPossibility) {
if (!bestPath || combinedPossibility > bestPath.possibility) {
bestPath = path;
}
}
+6 -1
View File
@@ -519,7 +519,12 @@ export class Arbiter {
_collectRelationUsages(rule, acc = new Map()) {
if (!rule || typeof rule !== 'object') return acc;
if (rule.type === 'direct') {
if (rule.type === 'direct' || (!rule.type && !rule.union && !rule.intersection && !rule.exclusion && (rule.relation || rule.rel || rule.label || rule.name))) {
// Shorthand operand objects ({ relation: 'owner' } inside union/
// intersection/exclusion) are direct usages too; failing to register
// them left the dependency index empty for logical rules, so
// invalidateRuleResultCacheByRelation never cleared their cached
// decisions after relation writes.
const relName = rule.relation || rule.rel || rule.label || rule.name;
if (relName) {
const reverse = rule.reverse === true;