2026-08-03 08:48:39 -07:00
|
|
|
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 {
|
2026-08-03 16:35:19 -07:00
|
|
|
constructor(arbiter, options = {}) {
|
2026-08-03 08:48:39 -07:00
|
|
|
this.arbiter = arbiter;
|
|
|
|
|
this.parser = parse;
|
2026-08-03 16:35:19 -07:00
|
|
|
this.generator = new RuleGenerator(arbiter, options);
|
2026-08-03 08:48:39 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|