evidence-dsl: extract Evidence DSL v2 compiler from @arbiter/core
CI / test (push) Successful in 11s
CI / publish (push) Successful in 9s

The Evidence DSL (ADR-000) is a thin declarative layer that compiles to
engine rule types. It has zero runtime coupling to the core engine
(DSLCompiler takes an arbiter as a duck-typed argument; the only shared
code was the ip-utils helpers, now local). Extracting it into its own
package keeps the core artifact free of the DSL surface.

- @arbiter/evidence-dsl depends on @arbiter/core (config formats are the
  compilation target)
- deep-path exports for the compiler, parser, generator, validation,
  and built-in functions (the surface the core's DSL tests consume)
- tests moved alongside; generate-parser script + peggy devDep local
- CI: test on push, publish on v* tags
This commit is contained in:
John Dvorak
2026-08-03 08:48:39 -07:00
commit ae21605fb7
54 changed files with 26551 additions and 0 deletions
+356
View File
@@ -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;
}
}