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:
@@ -0,0 +1,356 @@
|
||||
import { parse } from './parser/DSLParser.js';
|
||||
import { RuleGenerator } from './generator/RuleGenerator.js';
|
||||
import { validateDslText } from './validation/DSLValidation.js';
|
||||
|
||||
/**
|
||||
* DSL Compiler - Main integration layer
|
||||
* Compiles DSL text into rule configurations for the zanzibar-graph system
|
||||
*/
|
||||
export class DSLCompiler {
|
||||
constructor(arbiter) {
|
||||
this.arbiter = arbiter;
|
||||
this.parser = parse;
|
||||
this.generator = new RuleGenerator(arbiter);
|
||||
this.compiledPrograms = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile DSL text into rule configurations
|
||||
* @param {string} dslText - DSL text to compile
|
||||
* @param {string} programName - Optional name for the program
|
||||
* @returns {Object} Compilation result
|
||||
*/
|
||||
compile(dslText, programName = 'default') {
|
||||
try {
|
||||
const validation = validateDslText(dslText);
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: null,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
const program = validation.program;
|
||||
|
||||
// Convert plain AST to ProgramNode structure
|
||||
const programNode = {
|
||||
definitions: program.body.filter(s => s.type === 'Definition'),
|
||||
facts: program.body.filter(s => s.type === 'Fact'),
|
||||
evidence: program.body.filter(s => s.type === 'Evidence'),
|
||||
measures: program.body.filter(s => s.type === 'Measure'),
|
||||
validate: () => ({ isValid: true, errors: [], warnings: [] })
|
||||
};
|
||||
|
||||
// Generate rules from AST
|
||||
const generationResult = this.generator.generateRules(programNode);
|
||||
|
||||
if (!generationResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: generationResult.errors,
|
||||
warnings: [],
|
||||
program: programNode,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
// Store compiled program
|
||||
this.compiledPrograms.set(programName, {
|
||||
program: programNode,
|
||||
generatedRules: this.generator.getGeneratedRules(),
|
||||
dependencyIndex: this.generator.getDependencyIndex(),
|
||||
compiledAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: programNode,
|
||||
generatedRules: this.generator.getGeneratedRules(),
|
||||
dependencyIndex: this.generator.getDependencyIndex(),
|
||||
generatedCount: generationResult.generatedCount
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [`Compilation error: ${error.message}`],
|
||||
warnings: [],
|
||||
program: null,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile multiple DSL programs
|
||||
* @param {Object} programs - Map of program names to DSL text
|
||||
* @returns {Object} Compilation result for all programs
|
||||
*/
|
||||
compileMultiple(programs) {
|
||||
const results = {};
|
||||
let overallSuccess = true;
|
||||
const allErrors = [];
|
||||
const allWarnings = [];
|
||||
|
||||
for (const [name, dslText] of Object.entries(programs)) {
|
||||
const result = this.compile(dslText, name);
|
||||
results[name] = result;
|
||||
|
||||
if (!result.success) {
|
||||
overallSuccess = false;
|
||||
}
|
||||
|
||||
allErrors.push(...result.errors.map(err => `${name}: ${err}`));
|
||||
allWarnings.push(...result.warnings.map(warn => `${name}: ${warn}`));
|
||||
}
|
||||
|
||||
return {
|
||||
success: overallSuccess,
|
||||
errors: allErrors,
|
||||
warnings: allWarnings,
|
||||
results: results
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compiled program by name
|
||||
* @param {string} programName - Name of the program
|
||||
* @returns {Object|null} Compiled program or null
|
||||
*/
|
||||
getCompiledProgram(programName) {
|
||||
return this.compiledPrograms.get(programName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all compiled programs
|
||||
* @returns {Map} Map of all compiled programs
|
||||
*/
|
||||
getAllCompiledPrograms() {
|
||||
return this.compiledPrograms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove compiled program
|
||||
* @param {string} programName - Name of the program to remove
|
||||
* @returns {boolean} True if removed successfully
|
||||
*/
|
||||
removeCompiledProgram(programName) {
|
||||
return this.compiledPrograms.delete(programName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all compiled programs
|
||||
*/
|
||||
clearCompiledPrograms() {
|
||||
this.compiledPrograms.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compilation statistics
|
||||
* @returns {Object} Compilation statistics
|
||||
*/
|
||||
getCompilationStats() {
|
||||
const stats = {
|
||||
totalPrograms: this.compiledPrograms.size,
|
||||
totalRules: 0,
|
||||
programs: {}
|
||||
};
|
||||
|
||||
this.compiledPrograms.forEach((program, name) => {
|
||||
const programStats = {
|
||||
name: name,
|
||||
compiledAt: program.compiledAt,
|
||||
ruleCount: program.generatedRules.size,
|
||||
definitions: program.program.definitions.length,
|
||||
facts: program.program.facts.length,
|
||||
evidence: program.program.evidence.length,
|
||||
measures: program.program.measures.length
|
||||
};
|
||||
|
||||
stats.programs[name] = programStats;
|
||||
stats.totalRules += program.generatedRules.size;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate DSL text without compiling
|
||||
* @param {string} dslText - DSL text to validate
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
validate(dslText) {
|
||||
const validation = validateDslText(dslText);
|
||||
return {
|
||||
success: validation.success,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: validation.program
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parser errors from last parse
|
||||
* @returns {string[]} Array of parser errors
|
||||
*/
|
||||
getParserErrors() {
|
||||
return this.parser.getErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get generator errors from last generation
|
||||
* @returns {string[]} Array of generator errors
|
||||
*/
|
||||
getGeneratorErrors() {
|
||||
return this.generator.getErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a relation is configured
|
||||
* @param {string} relation - Relation name to check
|
||||
* @returns {boolean} True if relation is configured
|
||||
*/
|
||||
isRelationConfigured(relation) {
|
||||
return this.arbiter.relationConfigs.has(relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relation configuration
|
||||
* @param {string} relation - Relation name
|
||||
* @returns {Object|null} Relation configuration or null
|
||||
*/
|
||||
getRelationConfig(relation) {
|
||||
return this.arbiter.relationConfigs.get(relation) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured relations
|
||||
* @returns {Map} Map of all relation configurations
|
||||
*/
|
||||
getAllRelationConfigs() {
|
||||
return this.arbiter.relationConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export compiled program to JSON
|
||||
* @param {string} programName - Name of the program to export
|
||||
* @returns {string|null} JSON string or null if program not found
|
||||
*/
|
||||
exportProgram(programName) {
|
||||
const program = this.getCompiledProgram(programName);
|
||||
if (!program) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
name: programName,
|
||||
compiledAt: program.compiledAt,
|
||||
program: this.serializeProgram(program.program),
|
||||
generatedRules: Array.from(program.generatedRules.entries())
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import compiled program from JSON
|
||||
* @param {string} jsonString - JSON string to import
|
||||
* @returns {boolean} True if imported successfully
|
||||
*/
|
||||
importProgram(jsonString) {
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
const program = this.deserializeProgram(data.program);
|
||||
|
||||
this.compiledPrograms.set(data.name, {
|
||||
program: program,
|
||||
generatedRules: new Map(data.generatedRules),
|
||||
compiledAt: new Date(data.compiledAt)
|
||||
});
|
||||
|
||||
// Apply rules to arbiter
|
||||
data.generatedRules.forEach(([relation, config]) => {
|
||||
this.arbiter.setRelationConfig(relation, config);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize program to plain object
|
||||
* @param {ProgramNode} program - Program to serialize
|
||||
* @returns {Object} Serialized program
|
||||
*/
|
||||
serializeProgram(program) {
|
||||
// This is a simplified serialization - in a real implementation,
|
||||
// you'd want to properly serialize all node types
|
||||
return {
|
||||
type: 'Program',
|
||||
definitions: program.definitions.map(def => ({
|
||||
type: 'Definition',
|
||||
name: def.name,
|
||||
definitionType: def.definitionType,
|
||||
fields: def.fields.map(field => ({
|
||||
type: 'Field',
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
isArray: field.isArray,
|
||||
isOptional: field.isOptional
|
||||
}))
|
||||
})),
|
||||
facts: program.facts.map(fact => ({
|
||||
type: 'Fact',
|
||||
name: fact.name,
|
||||
parameters: fact.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
}))
|
||||
})),
|
||||
evidence: program.evidence.map(ev => ({
|
||||
type: 'Evidence',
|
||||
name: ev.name,
|
||||
parameters: ev.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
})),
|
||||
returnType: ev.returnType
|
||||
})),
|
||||
measures: program.measures.map(measure => ({
|
||||
type: 'Measure',
|
||||
name: measure.name,
|
||||
parameters: measure.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
})),
|
||||
returnType: measure.returnType
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize program from plain object
|
||||
* @param {Object} data - Serialized program data
|
||||
* @returns {ProgramNode} Deserialized program
|
||||
*/
|
||||
deserializeProgram(data) {
|
||||
// This is a simplified deserialization - in a real implementation,
|
||||
// you'd want to properly deserialize all node types
|
||||
const program = new ProgramNode();
|
||||
|
||||
// Note: This is a basic implementation. In practice, you'd need
|
||||
// to properly reconstruct all the AST nodes from the serialized data
|
||||
|
||||
return program;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user