js-rigor: remove the compiled evaluator — the runtime compiler is gone

The compiled evaluation path was never a performance win and was a
double-implementation liability: every semantics fix had to land twice
(CompiledEvaluator + LogicalOperators/handlers), and several bugs lived
only in one copy. A warm benchmark shows the compiled path at parity at
best (the apparent 7x chain regression was cold-cache confound).

Removed the runtime compiled dispatch entirely: RuleEvaluator evaluates
every rule through the single fallback path (logical operators + rule
handlers). The RuleCompiler remains as the config VALIDATOR only
(_compileErrors/_compileWarnings + _needsValues + the _compiled metadata
carried by snapshots). CompiledEvaluator.js deleted.

Fixes surfaced by removing the mask:
- The defeasible fallback wrap produced the wrong component shape
  ({rules} instead of {union:{rules}}/{intersection:{rules}}) — the
  compiled path always ran for defeasible configs, so the fallback had
  never executed; now wrapped correctly.
- Defeasible configs had no normal-dispatch routing (the compiled
  evaluator handled them); routed to evaluateDefeasible.
- The binary defeasible path forced the binary mode's internal 0.5
  threshold, while the compiled path always ran normal mode — the binary
  decision is now the thresholded normal combination (preserving the
  pinned contract).
- The fallback union/intersection/exclusion results now carry the
  validity blocks (previously only the compiled versions did).
This commit is contained in:
John Dvorak
2026-08-02 10:24:30 -07:00
parent faa6485e26
commit dcd90840d7
3 changed files with 42 additions and 927 deletions
+29 -28
View File
@@ -8,7 +8,6 @@ import { RelationalComparatorRouter } from './rules/RelationalComparatorRouter.j
import { ChainRule } from './rules/ChainRule.js';
import { ChallengeRule } from './rules/ChallengeRule.js';
import { ValueContext } from './ValueContext.js';
import { CompiledEvaluator } from './CompiledEvaluator.js';
export class RuleEvaluator {
constructor(arbiter) {
@@ -26,7 +25,6 @@ export class RuleEvaluator {
};
this.logicalOperators = new LogicalOperators(arbiter, this);
this.compiledEvaluator = new CompiledEvaluator(arbiter, this, this.logicalOperators);
this._valueRequirementCache = new Map();
}
@@ -70,15 +68,6 @@ export class RuleEvaluator {
return this._evaluateRuleBinary(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
}
if (rule && rule._compiled && options.useCompiled !== false && !(rule._compileErrors && rule._compileErrors.length)) {
const result = this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, enhancedOptions);
if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
finalValueContext.addCollectedValues(result.collectedValues, ruleType, rule);
}
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
}
if (rule.union) {
const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
@@ -94,6 +83,17 @@ export class RuleEvaluator {
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
}
// Defeasible configs ({ type: 'defeasible', when/unless/never/always })
// have no rule-handler entry; route them to the defeasible evaluator
// with the components normalized to the union/intersection format.
if (this.logicalOperators._isDefeasibleLogic(rule)) {
const result = this.logicalOperators.evaluateDefeasible(
numericUserId, userKey, numericObjectId, objectKey,
wrapDefeasibleComponents(rule), visited, currentRelation, enhancedOptions
);
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
}
// For other rule types, evaluate normally first
// Shorthand operand objects ({ relation: 'owner' } inside logical rules,
// or caller-supplied raw configs) carry no type: treat them as direct
@@ -292,17 +292,17 @@ export class RuleEvaluator {
// defeasible config). Route them through the defeasible evaluator,
// which implements binary/threshold modes natively.
if (this.logicalOperators._isDefeasibleLogic(rule)) {
if (rule._compiled && !(rule._compileErrors && rule._compileErrors.length)) {
return this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, {
...enhancedOptions,
binary: true,
fastPath: true
});
}
return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapDefeasibleComponents(rule), visited, currentRelation, {
// The compiled path ran defeasible rules in normal mode regardless of
// the binary flag (compiled.mode defaulted to 'normal'), so the binary
// decision was always the thresholded normal combination. Preserve
// that contract: force normal mode; the checker applies the binary
// thresholds to the result.
const wrapped = wrapDefeasibleComponents(rule);
wrapped.mode = 'normal';
return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapped, visited, currentRelation, {
...enhancedOptions,
binary: true,
fastPath: true
binary: false,
fastPath: false
});
}
@@ -433,18 +433,19 @@ export class RuleEvaluator {
* Logical (union/intersection/exclusion) components pass through.
*/
function wrapDefeasibleComponents(rule) {
const wrapSingle = (component) => ({ rules: [component] });
// The defeasible evaluator's normalize expects the component format
// when.intersection.rules[] / unless.union.rules[] (not bare {rules}).
const wrapUnion = (component) => {
if (!component) return component;
if (Array.isArray(component)) return { rules: component };
if (component.union || component.intersection || component.exclusion) return component;
return wrapSingle(component);
if (component.union) return component;
if (Array.isArray(component)) return { union: { rules: component } };
return { union: { rules: [component] } };
};
const wrapIntersection = (component) => {
if (!component) return component;
if (Array.isArray(component)) return { rules: component };
if (component.union || component.intersection || component.exclusion) return component;
return wrapSingle(component);
if (component.intersection) return component;
if (Array.isArray(component)) return { intersection: { rules: component } };
return { intersection: { rules: [component] } };
};
return {
...rule,