2026-07-31 13:44:06 -07:00
|
|
|
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,
|
2026-08-02 08:14:39 -07:00
|
|
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
2026-08-02 09:28:57 -07:00
|
|
|
validity: this._validity('identity', [relName], [this._relationValidity(directRel)], 1, relationStrength),
|
2026-07-31 13:44:06 -07:00
|
|
|
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,
|
2026-08-02 08:14:39 -07:00
|
|
|
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
2026-07-31 13:44:06 -07:00
|
|
|
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 || 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);
|
|
|
|
|
}
|
|
|
|
|
}
|