initial commit: @arbiter/core authorization engine with js-rigor hardening

Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/
binary modes, condensed snapshots, value relations) with 39 rigor test
campaigns. Includes fixes for snapshot binary writer/reader format
mismatch (snapshot-of-snapshot corruption), possibility write-boundary
validation, empty-graph snapshot serialization, relation lookup cache
direction collision, config-redefinition cache invalidation, binary
threshold semantics, defeasible compiled routing, and comparator
reason whitelisting.
This commit is contained in:
John Dvorak
2026-07-31 13:44:06 -07:00
commit 717ae1031e
373 changed files with 654131 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for fusion statements
* Represents: fusion max { ... }
*/
export class FusionNode extends BaseNode {
constructor(strategy, location = null) {
super('Fusion', location);
this.strategy = strategy; // 'max', 'min', 'majority', 'average', etc.
this.evidence = []; // Array of evidence statements
this.weights = null; // Optional weights array
}
/**
* Add evidence to this fusion
* @param {BaseNode} evidence - Evidence to add
*/
addEvidence(evidence) {
this.evidence.push(evidence);
this.addChild(evidence);
}
/**
* Set weights for this fusion
* @param {number[]} weights - Weights array
*/
setWeights(weights) {
this.weights = weights;
}
/**
* Get the fusion strategy
* @returns {string} Fusion strategy
*/
getStrategy() {
return this.strategy;
}
/**
* Get all evidence statements
* @returns {BaseNode[]} Evidence statements
*/
getEvidence() {
return this.evidence;
}
/**
* Get the weights for this fusion
* @returns {number[]|null} Weights or null
*/
getWeights() {
return this.weights;
}
/**
* Check if this fusion has weights
* @returns {boolean} True if has weights
*/
hasWeights() {
return this.weights !== null && this.weights.length > 0;
}
/**
* Check if this is a max fusion
* @returns {boolean} True if max fusion
*/
isMax() {
return this.strategy === 'max';
}
/**
* Check if this is a min fusion
* @returns {boolean} True if min fusion
*/
isMin() {
return this.strategy === 'min';
}
/**
* Check if this is a majority fusion
* @returns {boolean} True if majority fusion
*/
isMajority() {
return this.strategy === 'majority';
}
/**
* Check if this is an average fusion
* @returns {boolean} True if average fusion
*/
isAverage() {
return this.strategy === 'average';
}
/**
* Get the number of evidence statements
* @returns {number} Number of evidence statements
*/
getEvidenceCount() {
return this.evidence.length;
}
/**
* Validate the fusion
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate strategy
const validStrategies = [
'max',
'min',
'majority',
'average',
'sum',
'sum_unbounded',
'median',
'optimistic',
'pessimistic',
'top2',
'top3',
'priority',
'custom',
'count'
];
if (!validStrategies.includes(this.strategy)) {
errors.push(`Invalid fusion strategy: ${this.strategy}`);
}
// Validate evidence
if (this.evidence.length === 0) {
errors.push('Fusion must have at least one evidence statement');
}
// Validate each evidence statement
this.evidence.forEach((ev, index) => {
const evErrors = ev.validate ? ev.validate() : [];
errors.push(...evErrors.map(err => `Evidence ${index + 1}: ${err}`));
});
// Validate weights
if (this.weights !== null) {
if (!Array.isArray(this.weights)) {
errors.push('Weights must be an array');
} else if (this.weights.length !== this.evidence.length) {
errors.push('Weights array length must match evidence count');
} else if (this.weights.some(w => typeof w !== 'number' || w < 0)) {
errors.push('All weights must be non-negative numbers');
} else if (this.strategy === 'custom') {
const total = this.weights.reduce((sum, w) => sum + w, 0);
if (Math.abs(total - 1.0) > 1e-6) {
errors.push('Custom weights must sum to 1.0');
}
}
}
if (this.strategy === 'custom' && (!this.weights || this.weights.length === 0)) {
errors.push('Custom fusion requires weights');
}
return errors;
}
toString() {
const weightsStr = this.hasWeights() ? ` weights[${this.weights.length}]` : '';
return `Fusion(${this.strategy}, ${this.evidence.length} evidence${weightsStr})`;
}
}