ad9aa22225
- 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.
225 lines
7.6 KiB
JavaScript
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;
|
|
}
|
|
}
|