Files
core/src/core/arbiter/ArbiterConfig.js
T
John Dvorak ad9aa22225 js-rigor: partial-graph policy limits now enforce; binary-partial pins
- Arbiter constructor and setPartialGraphPolicy dropped maxNodes,
  maxRelations, and reservedRelations — the partial-graph context always
  saw the 1000/2000 defaults, silently disabling configured DoS guards.
  Both paths now carry the limits through; policy limits test pins
  constructor + setter enforcement and custom reservedRelations.
2026-07-31 15:34:45 -07:00

225 lines
7.6 KiB
JavaScript

export class ArbiterConfig {
constructor(arbiter) {
this.arbiter = arbiter;
}
registerDependencyIndex(index) {
if (!index) return;
this.arbiter.dependencyIndex = index;
}
setRelationConfig(relation, config) {
if (config === null || typeof config !== 'object') {
throw new Error(
`Invalid relation config for ${String(relation)}: expected a config object, got ${config === null ? 'null' : typeof config}`
);
}
const normalized = this._normalizeOwaConfig(config);
this.arbiter.relationConfigs.set(relation, normalized);
this.arbiter.graphManager.setRelationConfig(relation, normalized);
const compiledResult = this.arbiter.ruleCompiler.compile(relation, normalized);
normalized._compiled = compiledResult.compiled;
normalized._compileErrors = compiledResult.errors;
normalized._compileWarnings = compiledResult.warnings;
if (normalized && this.arbiter.authChecker && this.arbiter.authChecker.ruleEvaluator) {
normalized._needsValues = this.arbiter.authChecker.ruleEvaluator._ruleRequiresValues(normalized, new Set());
}
// Update dependency index for rule result cache invalidation (programmatic path)
this.arbiter._collectRelationUsages(normalized, this.arbiter.dependencyIndex);
// The direct-check cache is keyed by the CHECKED relation name; a
// redefinition changes that relation's semantics, so every cached
// decision for it must be dropped (e.g. direct r1 -> direct r2 would
// otherwise keep serving the r1 result until TTL expiry).
this.arbiter._invalidateDirectCheckCache(null, relation, null);
this.arbiter.invalidateRuleResultCacheByRelation(relation);
if (this.arbiter.authChecker) {
this.arbiter.authChecker.invalidateRuleCaches(relation);
}
// Track ChainRule dependencies (no materialization needed - PLTC handles reachability)
if (normalized && normalized.type === 'chain' && this.arbiter.reachabilityChecker) {
if (this.arbiter.reachabilityChecker.pltcIndex && this.arbiter.reachabilityChecker.pltcIndex.initialized) {
this.arbiter.reachabilityChecker._trackChainRuleDependencies(relation, normalized);
}
}
this._applyPartialGraphPolicyFromRelationConfig(relation, normalized);
}
setPartialGraphPolicy(policy = {}) {
const current = this.arbiter.partialGraphPolicy || { conflict_mode: 'deterministic', reducers: {} };
const next = policy && typeof policy === 'object' ? policy : {};
const conflictMode = next.conflict_mode || next.conflictMode || current.conflict_mode || 'deterministic';
const reducers = {
...(current.reducers || {}),
...((next.reducers && typeof next.reducers === 'object') ? next.reducers : {})
};
this.arbiter.partialGraphPolicy = {
conflict_mode: conflictMode,
reducers,
maxNodes: next.maxNodes !== undefined ? next.maxNodes : current.maxNodes,
maxRelations: next.maxRelations !== undefined ? next.maxRelations : current.maxRelations,
reservedRelations: next.reservedRelations !== undefined ? next.reservedRelations : current.reservedRelations
};
return this.arbiter.partialGraphPolicy;
}
getPartialGraphPolicy() {
const current = this.arbiter.partialGraphPolicy || { conflict_mode: 'deterministic', reducers: {} };
return {
conflict_mode: current.conflict_mode || 'deterministic',
reducers: { ...(current.reducers || {}) }
};
}
_applyPartialGraphPolicyFromRelationConfig(relation, config) {
if (!config || typeof config !== 'object') return;
const pg = config.partial_graph || config.partialGraph || null;
if (!pg || typeof pg !== 'object') return;
const patch = {};
if (pg.conflict_mode || pg.conflictMode) {
patch.conflict_mode = pg.conflict_mode || pg.conflictMode;
}
if (pg.reducer) {
patch.reducers = { [relation]: pg.reducer };
}
if (Object.keys(patch).length > 0) {
this.setPartialGraphPolicy(patch);
}
}
_normalizeOwaConfig(config) {
if (!config || typeof config !== 'object') return config;
const normalizeNode = (node) => {
if (!node || typeof node !== 'object') return;
if (node.union && typeof node.union === 'object') {
this._normalizeOwaWeights(node.union);
if (Array.isArray(node.union.rules)) {
node.union.rules.forEach(normalizeNode);
}
}
if (node.intersection && typeof node.intersection === 'object') {
this._normalizeOwaWeights(node.intersection);
if (Array.isArray(node.intersection.rules)) {
node.intersection.rules.forEach(normalizeNode);
}
}
if (node.exclusion && typeof node.exclusion === 'object') {
this._normalizeOwaWeights(node.exclusion);
if (Array.isArray(node.exclusion.rules)) {
node.exclusion.rules.forEach(normalizeNode);
}
}
if (node.type === 'relational_comparator') {
if (node.left) {
this._normalizeOwaWeights(node.left);
if (node.left.rule) normalizeNode(node.left.rule);
}
if (node.right) {
this._normalizeOwaWeights(node.right);
if (node.right.rule) normalizeNode(node.right.rule);
}
}
};
normalizeNode(config);
return config;
}
_normalizeOwaWeights(node) {
if (!node || typeof node !== 'object' || !Array.isArray(node.owaWeights)) return;
if (node.owaWeights.length === 0) return;
const aggregator = node.aggregator;
if (aggregator && aggregator !== 'owa') return;
const detected = this._detectOwaMode(node.owaWeights);
if (detected) {
node.aggregator = detected.mode;
delete node.owaWeights;
return;
}
const sparseCount = this._detectOwaSparseCount(node.owaWeights);
if (sparseCount) {
node._owaSparseCount = sparseCount;
node.owaWeights._owaSparseCount = sparseCount;
}
}
_detectOwaMode(weights) {
const eps = 1e-9;
const n = weights.length;
if (n === 0) return null;
let sum = 0;
for (let i = 0; i < n; i++) {
sum += weights[i];
}
if (Math.abs(sum - 1) > 1e-6) return null;
const isZero = (v) => Math.abs(v) <= eps;
const isOne = (v) => Math.abs(v - 1) <= eps;
const isEqual = (a, b) => Math.abs(a - b) <= eps;
if (isOne(weights[0]) && weights.slice(1).every(isZero)) {
return { mode: 'max' };
}
if (isOne(weights[n - 1]) && weights.slice(0, n - 1).every(isZero)) {
return { mode: 'min' };
}
const uniform = weights.every((v) => isEqual(v, weights[0]));
if (uniform && isEqual(weights[0] * n, 1)) {
return { mode: 'average' };
}
if (n >= 2) {
const top2Weight = weights[0];
if (!isZero(top2Weight) && isEqual(weights[1], top2Weight)) {
const restZero = weights.slice(2).every(isZero);
if (restZero && isEqual(top2Weight * 2, 1)) {
return { mode: 'top2' };
}
}
}
if (n >= 3) {
const top3Weight = weights[0];
if (!isZero(top3Weight) && isEqual(weights[1], top3Weight) && isEqual(weights[2], top3Weight)) {
const restZero = weights.slice(3).every(isZero);
if (restZero && isEqual(top3Weight * 3, 1)) {
return { mode: 'top3' };
}
}
}
return null;
}
_detectOwaSparseCount(weights) {
const eps = 1e-9;
let lastNonZero = -1;
for (let i = 0; i < weights.length; i++) {
if (Math.abs(weights[i]) > eps) lastNonZero = i;
}
const sparseCount = lastNonZero + 1;
if (sparseCount <= 0) return null;
if (sparseCount > 3) return null;
for (let i = sparseCount; i < weights.length; i++) {
if (Math.abs(weights[i]) > eps) return null;
}
return sparseCount;
}
}