440230b2c5
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.
134 lines
4.8 KiB
JavaScript
134 lines
4.8 KiB
JavaScript
import { BaseRule } from './BaseRule.js';
|
|
import { Arbiter } from '../../core/Arbiter.js';
|
|
|
|
/**
|
|
* DirectRule - Checks for direct relationships between user and object
|
|
*
|
|
* This rule evaluates direct relations in the graph, optionally in reverse direction.
|
|
* It supports efficient batch processing and early exit optimizations.
|
|
*
|
|
* Returns standardized results with raw possibility values:
|
|
* - possibility_allow: The strength/possibility of the relation (0.0 to 1.0)
|
|
* - possibility_deny: Always 0 (DirectRule only reports relation strength, not polarity)
|
|
* - Collected Values: Values from relations with full path metadata
|
|
*
|
|
* Note: This rule returns raw relation strength. The logical context (defeater, strict,
|
|
* defeasible, etc.) determines how this strength is interpreted as positive or negative evidence.
|
|
*
|
|
* Configuration:
|
|
* {
|
|
* type: 'direct',
|
|
* relation: string, // Optional: relation to check (uses currentRelation if not specified)
|
|
* reverse: boolean, // Optional: check in reverse direction (default: false)
|
|
* collectValues: boolean // Optional: collect values from relations (default: true if relation has values)
|
|
* }
|
|
*/
|
|
export class DirectRule extends BaseRule {
|
|
constructor(arbiter) {
|
|
super(arbiter);
|
|
}
|
|
|
|
/**
|
|
* Evaluate method returns raw relation strength
|
|
*/
|
|
evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
const { fastPath = false, minPossibility = null, collectValues: collectValuesOption, includeMeta = true } = options;
|
|
|
|
const relName = rule.relation || rule.rel || rule.label || rule.name || currentRelation;
|
|
const reverse = rule.reverse;
|
|
const collectValues = collectValuesOption !== undefined ? collectValuesOption : rule.collectValues !== false;
|
|
|
|
let directRel;
|
|
if (reverse) {
|
|
directRel = this.arbiter.relationManager.getDirectRelation(objectId, relName, userId, options);
|
|
} else {
|
|
directRel = this.arbiter.relationManager.getDirectRelation(userId, relName, objectId, options);
|
|
}
|
|
|
|
if (!directRel) {
|
|
return this._createStandardResult({
|
|
possibility: 0,
|
|
...(includeMeta && {
|
|
meta: {
|
|
ruleType: 'direct',
|
|
reason: 'no_relation'
|
|
}
|
|
})
|
|
}, []);
|
|
}
|
|
|
|
const relationStrength = directRel.possibility;
|
|
const _source = directRel.source || 'persistent';
|
|
const _allowMeta = {
|
|
ruleType: 'direct',
|
|
reason: 'direct',
|
|
source: _source,
|
|
layer_name: directRel.layer_name || null,
|
|
source_class: directRel.source_class || null,
|
|
reducer_applied: directRel.reducer_applied || null
|
|
};
|
|
|
|
const authResult = {
|
|
possibility: relationStrength,
|
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
|
validity: this._validity('identity', [relName], [this._relationValidity(directRel)], 1, relationStrength),
|
|
possibility_allow: relationStrength, // For binary mode
|
|
possibility_deny: 0, // DirectRule doesn't deny
|
|
...(includeMeta && {
|
|
meta: {
|
|
ruleType: 'direct',
|
|
reason: 'relation_exists',
|
|
rule,
|
|
relation: relName,
|
|
reverse: reverse || false,
|
|
strength: relationStrength,
|
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
|
source: _source,
|
|
allow: _allowMeta
|
|
},
|
|
meta_allow: _allowMeta
|
|
}),
|
|
reason: 'exists'
|
|
};
|
|
|
|
// Collect values if relation has them and collection is enabled
|
|
let collectedValues = [];
|
|
if (collectValues && directRel.value !== undefined) {
|
|
const sourceEntity = reverse ? objectKey : userKey;
|
|
const targetEntity = reverse ? userKey : objectKey;
|
|
const path = [sourceEntity, targetEntity];
|
|
|
|
collectedValues.push(this._createCollectedValue(
|
|
directRel.value,
|
|
directRel.possibility,
|
|
path,
|
|
{
|
|
entityKey: sourceEntity,
|
|
relation: relName,
|
|
step: 0
|
|
},
|
|
{
|
|
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'
|
|
}
|
|
));
|
|
}
|
|
|
|
// Apply early exit logic if thresholds are enabled
|
|
if (fastPath) {
|
|
// Early exit based on relation strength threshold
|
|
if (minPossibility !== null && authResult.possibility >= minPossibility) {
|
|
|
|
if (authResult.meta) {
|
|
authResult.meta.earlyExit = true;
|
|
authResult.meta.earlyExitReason = 'strength_threshold_met';
|
|
}
|
|
}
|
|
}
|
|
|
|
return this._createStandardResult(authResult, collectedValues);
|
|
}
|
|
}
|