refactor: extract Evidence DSL to @arbiter/evidence-dsl package
CI / test (push) Successful in 5m23s
CI / benchmark (push) Successful in 47s
CI / publish (push) Has been skipped

The Evidence DSL (ADR-000) compiles a natural DSL into core rule
configurations — it is a separate concern from the engine. The AST had
zero runtime coupling to the core (DSLCompiler takes the arbiter as a
duck-typed argument; ip-utils were the only shared code, now local to
the DSL package). This extraction removes the DSL surface from the core
artifact entirely:

- src/ast/ (748K, ~60 files) moved to @arbiter/evidence-dsl@1.0.0
- ip-utils moved with it (only the DSL consumed them)
- generate-parser script + peggy devDep moved to the DSL package
- the 8 DSL-consuming tests now import from @arbiter/evidence-dsl
  (deep-path exports: DSLCompiler, parser/*, generator/*, validation/*,
  interpreter/*)
- package.json gains the devDependency, drops build:ast/generate:parser

Tarball: AST-free. Rigor 251/251, full suite 838/776/0.
This commit is contained in:
John Dvorak
2026-08-03 09:17:33 -07:00
parent f8f6c5cb1b
commit f446750ff3
59 changed files with 34 additions and 26402 deletions
+21 -53
View File
@@ -1,12 +1,12 @@
{ {
"name": "@arbiter/core", "name": "@arbiter/core",
"version": "1.0.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@arbiter/core", "name": "@arbiter/core",
"version": "1.0.0", "version": "1.0.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@tenere/pltc-core": "^0.6.3", "@tenere/pltc-core": "^0.6.3",
@@ -14,21 +14,32 @@
"uuidv7": "^1.0.2" "uuidv7": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@arbiter/evidence-dsl": "^1.0.0",
"@rigor/core": "*", "@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1", "@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3", "fast-check": "^4.5.3"
"peggy": "^5.0.6"
} }
}, },
"node_modules/@peggyjs/from-mem": { "node_modules/@arbiter/core": {
"version": "3.1.3", "version": "1.0.1",
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fcore/-/1.0.1/core-1.0.1.tgz",
"integrity": "sha512-BfIv6vRKsuJR39WBkBGxl2PQ/LDoW/J1u4UeFrog0AbeTSTvRqhxG1+its2pAcUvcQXq99ZOccFlK3XgNRKt4Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "ISC",
"dependencies": { "dependencies": {
"semver": "7.7.4" "@tenere/pltc-core": "^0.6.3",
"heapify": "^1.0.2",
"uuidv7": "^1.0.2"
}
}, },
"engines": { "node_modules/@arbiter/evidence-dsl": {
"node": ">=20.8" "version": "1.0.0",
"resolved": "https://hub.kl1.tenere.ai/api/packages/Arbiter/npm/%40arbiter%2Fevidence-dsl/-/1.0.0/evidence-dsl-1.0.0.tgz",
"integrity": "sha512-v28SkNfcR60rJkDG6Btp5ieYaHQQZEiIJshsyGZYJiTt+V2bGbEj8341PUSzaSE5vRL2mVmRWKT9RoDHQXnzWg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@arbiter/core": "^1.0.1"
} }
}, },
"node_modules/@rigor/analysis": { "node_modules/@rigor/analysis": {
@@ -332,14 +343,6 @@
"@tenere/graph-core": "^1.0.1" "@tenere/graph-core": "^1.0.1"
} }
}, },
"node_modules/commander": {
"version": "14.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/fast-check": { "node_modules/fast-check": {
"version": "4.9.0", "version": "4.9.0",
"dev": true, "dev": true,
@@ -365,22 +368,6 @@
"version": "1.0.2", "version": "1.0.2",
"license": "MIT" "license": "MIT"
}, },
"node_modules/peggy": {
"version": "5.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@peggyjs/from-mem": "3.1.3",
"commander": "^14.0.3",
"source-map-generator": "2.0.6"
},
"bin": {
"peggy": "bin/peggy.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/pure-rand": { "node_modules/pure-rand": {
"version": "8.4.2", "version": "8.4.2",
"dev": true, "dev": true,
@@ -396,25 +383,6 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": {
"version": "7.7.4",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map-generator": {
"version": "2.0.6",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=20"
}
},
"node_modules/uuidv7": { "node_modules/uuidv7": {
"version": "1.2.1", "version": "1.2.1",
"license": "Apache-2.0", "license": "Apache-2.0",
+1 -3
View File
@@ -32,8 +32,6 @@
"test:property": "node --test --test-force-exit \"tests/property-based/**/*.test.js\"", "test:property": "node --test --test-force-exit \"tests/property-based/**/*.test.js\"",
"test:integration": "node --test --test-force-exit \"tests/integration/*.js\"", "test:integration": "node --test --test-force-exit \"tests/integration/*.js\"",
"test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js", "test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js",
"generate:parser": "node scripts/generate-parser.js",
"build:ast": "npm run generate:parser",
"benchmark": "node --expose-gc scripts/benchmark.js", "benchmark": "node --expose-gc scripts/benchmark.js",
"benchmark:core": "node benchmarks/core-performance-benchmark.js", "benchmark:core": "node benchmarks/core-performance-benchmark.js",
"benchmark:batch": "node benchmarks/batch-size-analysis.js", "benchmark:batch": "node benchmarks/batch-size-analysis.js",
@@ -69,6 +67,6 @@
"@rigor/core": "*", "@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1", "@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3", "fast-check": "^4.5.3",
"peggy": "^5.0.6" "@arbiter/evidence-dsl": "^1.0.0"
} }
} }
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env node
/**
* Script to generate the DSL parser from Peggy grammar
*/
import peggy from 'peggy';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const grammarPath = path.join(__dirname, '../src/ast/grammar/dsl.peggy');
const outputPath = path.join(__dirname, '../src/ast/parser/GeneratedParser.js');
console.log('Generating DSL parser from Peggy grammar...');
console.log('Grammar:', grammarPath);
console.log('Output:', outputPath);
try {
// Read the grammar file
const grammar = fs.readFileSync(grammarPath, 'utf8');
// Generate the parser
const parserSource = peggy.generate(grammar, {
format: 'es',
output: 'source',
grammarSource: 'dsl.peggy'
});
// Write the generated parser
fs.writeFileSync(outputPath, parserSource);
console.log('✅ Parser generated successfully!');
console.log('Generated file:', outputPath);
} catch (error) {
console.error('❌ Error generating parser:');
console.error(error.message);
if (error.format) {
console.error('\nFormatted error:');
console.error(error.format([
{ source: 'dsl.peggy', text: fs.readFileSync(grammarPath, 'utf8') }
]));
}
process.exit(1);
}
-356
View File
@@ -1,356 +0,0 @@
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;
}
}
File diff suppressed because it is too large Load Diff
-420
View File
@@ -1,420 +0,0 @@
/*
* Peggy Parser for the Evidence DSL
*
* This grammar defines a declarative language for authorization policies.
* It parses definitions, facts, measures, and evidence rules into a structured
* Abstract Syntax Tree (AST) represented by plain JavaScript objects.
* (Version 4: Corrected infinite loop check in String literal parsing)
*/
{
// The location() function provides line/column info for error reporting.
// The text() function returns the matched text for a rule.
// Helper function to build a left-associative binary expression tree.
function buildLeftAssoc(head, tail) {
return tail.reduce((result, element) => {
return {
type: "BinaryExpression",
operator: element[1],
left: result,
right: element[3],
location: location()
};
}, head);
}
}
// -- Grammar Entry Point --
Program
= _ statements:(Statement _)* _ {
const allStatements = statements.map(s => s[0]);
return {
type: "Program",
body: allStatements,
definitions: allStatements.filter(s => s.type === "Definition"),
facts: allStatements.filter(s => s.type === "Fact"),
evidence: allStatements.filter(s => s.type === "Evidence"),
measures: allStatements.filter(s => s.type === "Measure"),
sources: allStatements.filter(s => s.type === "Source")
};
}
Statement
= Definition
/ Source
/ Fact
/ Evidence
/ Measure
// -- Top-Level Statements --
Definition "A type definition"
= ("definition" / "type") __ name:Identifier __ "{" _ fields:(Field _)* "}" {
return { type: "Definition", name, fields: fields.map(f => f[0]) };
}
Field
= name:Identifier _ ":" _ fieldType:Type _ isArray:("[]")? _ behavior:Behavior? _ cache:CacheDirective? {
return {
type: "Field",
name,
fieldType,
isArray: !!isArray,
behavior: behavior || null,
cache: cache || null
};
}
Fact "A statement of fact (or relation in ADR-000)"
= ("fact" / "relation") __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ behavior:BehaviorAnnotation? _ properties:(FactProperty _)* cache:CacheDirective? _ limit:Limit? {
return {
type: "Fact",
name,
params: params || [],
behavior: behavior || null,
properties: properties.map(p => p[0]),
cache: cache || null,
limit: limit || null,
injectable: !!star
};
}
Source "An injectable source (proof provider)"
= "source" __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ provides:Provides? _ within:WithinClause? {
return {
type: "Source",
name,
params: params || [],
provides: provides || null,
injectable: !!star,
within: within || null
};
}
WithinClause "A freshness constraint on a source"
= "within" __ duration:Duration { return duration; }
Evidence "An evidence rule"
= "evidence" __ star:"*"? name:Identifier _ "(" _ params:ParameterList? _ ")" _ limit:Limit? _ "{" _ body:EvidenceBody _ "}" _ provides:Provides? {
return {
type: "Evidence",
name,
params: params || [],
limit: limit || null,
body,
provides: provides || null,
challenge: !!star
};
}
Measure "A derived measurement or value"
= "measure" __ name:Identifier _ "(" _ params:ParameterList? _ ")" _ "{" _ body:MeasureBody _ "}" _ provides:Provides? {
return {
type: "Measure",
name,
params: params || [],
body,
provides: provides || null
};
}
// -- Evidence & Measure Internals --
EvidenceBody
= statements:(EvidenceStatement _)* {
return { type: "EvidenceBody", statements: statements.map(s => s[0]) };
}
EvidenceStatement
= DefeasibleLogic
/ Fusion
/ CollectionProcessing
/ PatternMatch
/ Expression
MeasureBody
= statements:(MeasureStatement _)* returnStmt:ReturnStatement? {
return {
type: "MeasureBody",
statements: statements.map(s => s[0]),
returnStatement: returnStmt || null
};
}
MeasureStatement
= Fusion
/ Aggregation
/ PatternMatch
/ Expression
ReturnStatement
= "return" __ expression:Expression {
return { type: "ReturnStatement", expression };
}
// -- Complex Statement Types --
DefeasibleLogic
= type:("NEVER" / "ALWAYS" / "REQUIRES") __ condition:Expression {
return { type: "DefeasibleLogic", logicType: type, condition };
}
/ "WHEN" __ condition:Expression __ "UNLESS" __ defeater:Expression {
return { type: "DefeasibleLogic", logicType: "WHEN", condition, defeater };
}
/ "WHEN" __ condition:Expression {
return { type: "DefeasibleLogic", logicType: "WHEN", condition };
}
PatternMatch
= predicate:PatternPredicate _ binding:BindingClause? _ "{" _ body:EvidenceBody _ "}" _ limit:Limit? _ withClause:WithClause? {
return {
type: "PatternMatch",
predicate,
binding: binding || null,
limit: limit || null,
body,
withClause: withClause || null
};
}
CollectionProcessing
= measure:Expression _ "|" _ variable:Identifier _ "|" _ fusionStrategy:("fusion" __ strategy:Identifier)? _ "{" _ body:EvidenceBody _ "}" _ limit:Limit? {
return {
type: "CollectionProcessing",
measure,
variable,
fusion: fusionStrategy ? { strategy: fusionStrategy[1] } : null,
body,
limit: limit || null
};
}
PatternPredicate
= name:Identifier _ "(" _ args:PatternArgumentList? _ ")" {
return { type: "Predicate", name, args: args || [] };
}
PatternArgumentList
= head:PatternArgument tail:(_ "," _ arg:PatternArgument)* {
return [head, ...tail.map(t => t[3])];
}
PatternArgument
= "*" _ name:Identifier { return { type: "Wildcard", name }; }
/ Expression
BindingClause
= "|" _ name:Identifier _ "|" { return name; }
WithClause
= "with" __ condition:Expression { return condition; }
Fusion
= "fusion" __ strategy:Identifier __ "{" _ expressions:ExpressionList _ "}" {
return { type: "Fusion", strategy, expressions };
}
Aggregation
= "aggregate" __ "{" _ expressions:ExpressionList _ "}" _ using:Using? {
return { type: "Aggregation", expressions, using: using || null };
}
Using
= "USING" __ method:Identifier { return method; }
// -- Type System & Parameters --
Type
= Identifier
TypeName
= name:Identifier { return { type: "TypeName", name }; }
/ literal:String { return { type: "TypeName", name: literal.value }; }
ParameterList
= head:Parameter tail:(_ "," _ param:Parameter)* {
return [head, ...tail.map(t => t[3])];
}
Parameter
= name:Identifier _ ":" _ paramType:Type _ isArray:("[]")? {
return { type: "Parameter", name, paramType, isArray: !!isArray };
}
Provides
= "PROVIDES" __ providesType:Type { return providesType; }
BehaviorAnnotation
= "BEHAVES" __ "AS" __ behavior:("edge" / "transitive" / "hierarchical" / "symmetrical_graph") {
return { type: "BehaviorAnnotation", behavior };
}
FactProperty
= "transitive" { return "transitive"; }
/ "symmetrical" { return "symmetrical"; }
Limit
= "limit" __ value:Integer { return value; }
// -- Behaviors and Caching --
Behavior
= "BEHAVES" __ "{" _ b:(DecayBehavior / BlurBehavior / TTLBehavior) _ "}" { return b; }
DecayBehavior
= "decaying" __ direction:("up" / "down" / "neutral" / "stable") __ period:("hourly" / "daily" / "weekly" / "monthly") {
return { type: "Behavior", behaviorType: "decay", direction, period };
}
BlurBehavior
= "blurring" __ mode:("fixed" / "adaptive" / "confidence") confidence:(__ ("confidence_90" / "confidence_95" / "confidence_99"))? {
return { type: "Behavior", behaviorType: "blur", mode, confidence: confidence ? confidence[1] : null };
}
TTLBehavior
= "ttl" __ duration:Duration {
return { type: "Behavior", behaviorType: "ttl", duration };
}
CacheDirective
= "CACHE" __ directive:("eager" / "lazy") { return directive; }
// -- Expressions (with operator precedence) --
Expression
= LogicalOr
LogicalOr
= head:LogicalAnd tail:(_ "||" _ right:LogicalAnd)* { return buildLeftAssoc(head, tail); }
LogicalAnd
= head:Comparison tail:(_ "&&" _ right:Comparison)* { return buildLeftAssoc(head, tail); }
Comparison
= head:TemporalComparison _ "is" __ typeName:TypeName {
return { type: "BinaryExpression", operator: "is", left: head, right: typeName };
}
/ head:TemporalComparison tail:(_ operator:("==" / "!=" / ">=" / "<=" / ">" / "<") _ right:TemporalComparison)* { return buildLeftAssoc(head, tail); }
TemporalComparison
= head:Addition _ "within" __ right:Duration {
return { type: "BinaryExpression", operator: "within", left: head, right };
}
/ Addition
Addition
= head:Multiplication tail:(_i operator:("+" / "-") _i right:Multiplication)* { return buildLeftAssoc(head, tail); }
Multiplication
= head:Unary tail:(_i operator:("*" / "/") _i right:Unary)* { return buildLeftAssoc(head, tail); }
Unary
= operator:("NOT" / "!") __ operand:Unary { return { type: "UnaryExpression", operator: "NOT", operand }; }
/ Postfix
Postfix
= primary:(AttributeAccess / PrimaryTerm) binding:BindingClause? {
if (binding) {
return { type: "BindingAccess", expression: primary, binding };
}
return primary;
}
AttributeAccess
= head:PrimaryTerm tail:(_ "." _ attr:Identifier)+ {
return tail.reduce((obj, part) => {
return {
type: "AttributeAccess",
object: obj,
attribute: part[3], // The Identifier is the 4th element (index 3)
location: location()
};
}, head);
}
PrimaryTerm "The non-recursive base for an expression chain"
= ChallengePredicate
/ Literal
/ PredicateCall
/ Variable
/ "(" _ expr:Expression _ ")" { return expr; }
ChallengePredicate
= "*" name:Identifier _ "(" _ args:ArgumentList? _ ")" {
return { type: "PredicateCall", name, args: args || [], challenge: true };
}
PredicateCall
= name:Identifier "(" _ args:ArgumentList? _ ")" {
return { type: "PredicateCall", name, args: args || [] };
}
Variable
= name:Identifier { return { type: "Variable", name }; }
ArgumentList
= head:Expression tail:(_ "," _ expr:Expression)* {
return [head, ...tail.map(t => t[3])];
}
ExpressionList
= head:Expression tail:(_ "," _ expr:Expression)* {
return [head, ...tail.map(t => t[3])];
}
// -- Literals --
Literal
= String / Float / Integer / Boolean / Duration
String "A string literal"
= '"' chars:((!("\"" / "\\")) . / "\\" .)* '"' {
return { type: "Literal", value: JSON.parse(text()) };
}
/ "'" chars:((!("'" / "\\")) . / "\\" .)* "'" {
return { type: "Literal", value: JSON.parse("\"" + chars.map(c => c[0] === '\\' ? c[1] : c[1]).join('') + "\"") };
}
Float "A floating-point number"
= value:([0-9]+ "." [0-9]+) { return { type: "Literal", value: parseFloat(text()) }; }
Integer "An integer"
= value:[0-9]+ { return { type: "Literal", value: parseInt(text(), 10) }; }
Boolean "A boolean literal"
= value:("true" / "false") { return { type: "Literal", value: value === "true" }; }
Duration "A time duration literal"
= value:([0-9]+ ("h" / "d" / "w" / "m")) { return { type: "Literal", value: text(), unit: text().slice(-1) }; }
// -- Core Tokens & Whitespace --
Identifier
= !Keyword name:$([a-zA-Z_][a-zA-Z0-9_]*) { return name; }
Keyword
= ("definition" / "type" / "fact" / "relation" / "evidence" / "measure" / "BEHAVES" / "AS" / "CACHE"
/ "decaying" / "blurring" / "ttl" / "transitive" / "symmetrical" / "hierarchical" / "symmetrical_graph" / "edge" / "limit"
/ "PROVIDES" / "fusion" / "aggregate" / "USING" / "NEVER" / "ALWAYS" / "WHEN" / "UNLESS"
/ "REQUIRES" / "with" / "true" / "false" / "NOT" / "within" / "return" / "is") !([a-zA-Z0-9_])
// _ = optional whitespace and comments
// __ = mandatory whitespace and comments
_
= (WhiteSpace / Comment)*
// Inline (single-line) optional whitespace — used around arithmetic
// operators so a `*` challenge-predicate on the next line is not
// absorbed as a multiplication tail.
_i
= [ \t]*
__
= (WhiteSpace / Comment)+
WhiteSpace
= [ \t\r\n]
Comment
= "//" [^\r\n]*
/ "/*" (!"*/" .)* "*/"
-167
View File
@@ -1,167 +0,0 @@
// Inline Expression Grammar for Permission Checking
//
// This grammar parses inline DSL expressions used for permission checks.
// It supports predicate calls, OWA Fusion blocks (exclusive composition),
// challenge predicates (* prefix for out-of-band), and defeasible logic (UNLESS).
// AND/OR operators removed — OWA Fusion is the only composition mechanism.
//
// Usage: npx peggy -o src/ast/parser/ExpressionParser.js src/ast/grammar/expression.peggy
{
// Helper functions
function makeVariable(name, path) {
return { type: 'Variable', name, path: path || [] };
}
function makePredicate(name, args) {
return { type: 'Predicate', name, args: args || [] };
}
function makeChallengePredicate(name, args) {
return { type: 'Predicate', name, args: args || [], challenge: true };
}
function makeFusion(expressions, aggregator) {
return { type: 'Fusion', aggregator, expressions };
}
function makeDefeasible(primary, exception) {
return { type: 'Defeasible', primary, exception };
}
}
// Entry point
Expression
= _ expr:DefeasibleExpr _ { return expr; }
// Defeasible logic: primary UNLESS exception
DefeasibleExpr
= primary:PrimaryExpr _ "UNLESS" _ exception:PredicateCall {
return makeDefeasible(primary, exception);
}
/ PrimaryExpr
// Primary expressions: Fusion blocks or predicate calls
PrimaryExpr
= FusionBlock
/ ChallengePredicate
/ PredicateCall
// OWA Fusion block: FUSION <aggregator> { expr1 expr2 ... }
FusionBlock
= "FUSION" _ aggregator:AggregatorKeyword _ "{" _ expressions:ExpressionList _ "}" {
return makeFusion(expressions, aggregator);
}
// Aggregator keywords (subset of ADR-000 DSL v2 aggregators)
AggregatorKeyword
= "max" / "min" / "majority" / "average" / "sum" / "sum_unbounded"
/ "median" / "optimistic" / "pessimistic" / "top2" / "top3" / "priority"
// List of expressions (whitespace-separated)
ExpressionList
= head:Expression tail:(_ Expression)* {
const exprs = [head];
for (const t of tail) {
exprs.push(t[1]);
}
return exprs;
}
// Challenge predicate (* prefixed): *name(arg1, arg2, ...)
ChallengePredicate
= "*" name:Identifier _ "(" _ args:ArgumentList? _ ")" {
return makeChallengePredicate(name, args || []);
}
// Predicate call: name(arg1, arg2, ...)
PredicateCall
= name:Identifier _ "(" _ args:ArgumentList? _ ")" {
return makePredicate(name, args || []);
}
// Comma-separated arguments
ArgumentList
= head:Argument tail:(_ "," _ Argument)* {
const args = [head];
for (const t of tail) {
args.push(t[3]);
}
return args;
}
// Argument types
// Order matters: try VariableBinding first (starts with :),
// then Literal (strings/numbers), then TypedReference (which looks like an identifier)
Argument
= VariableBinding
/ Literal
/ TypedReference
// Variable binding: :name or :name.path.subpath
VariableBinding
= ":" name:Identifier path:("." Identifier)* {
return makeVariable(name, path.map(p => p[1]));
}
// Typed reference: Type::path.subpath (e.g., document::params.id)
TypedReference
= refType:Identifier "::" path:Path {
return { type: 'Reference', refType: refType, path: path };
}
// Path for typed references
Path
= head:Identifier tail:("." Identifier)* {
const parts = [head];
for (const t of tail) {
parts.push(t[1]);
}
return parts;
}
// Literals
Literal
= StringLiteral
/ NumberLiteral
// String literals (single or double quoted)
StringLiteral
= '"' chars:([^"\\] / EscapeSequence)* '"' {
return { type: 'Literal', value: chars.join(''), dataType: 'string' };
}
/ "'" chars:([^'\\] / EscapeSequence)* "'" {
return { type: 'Literal', value: chars.join(''), dataType: 'string' };
}
// Escape sequences
EscapeSequence
= "\\" char:["'\\nrt] {
const escapes = { '"': '"', "'": "'", '\\': '\\', 'n': '\n', 'r': '\r', 't': '\t' };
return escapes[char] || char;
}
// Number literals
NumberLiteral
= digits:([0-9]+) {
return { type: 'Literal', value: parseInt(digits.join(''), 10), dataType: 'number' };
}
// Identifiers (support hyphens like doc-123, user-456)
Identifier
= first:[a-zA-Z_] rest:[a-zA-Z0-9_-]* {
return first + rest.join('');
}
// Whitespace and comments
_ "whitespace"
= (WS / LineComment / BlockComment)*
WS
= [ \t\n\r]+
LineComment
= "//" [^\n]*
BlockComment
= "/*" (!"*/" .)* "*/"
-22
View File
@@ -1,22 +0,0 @@
/**
* AST Module - Main export file
* Provides access to all AST functionality for DSL compilation
*/
// Core AST components
export { DSLCompiler } from './DSLCompiler.js';
// Parser
export { PeggyDSLParser } from './parser/PeggyDSLParser.js';
// Generator
export { RuleGenerator } from './generator/RuleGenerator.js';
// Validation
export { validateDslText } from './validation/DSLValidation.js';
// All AST nodes
export * from './nodes/index.js';
// Re-export for convenience
export { DSLCompiler as default } from './DSLCompiler.js';
-182
View File
@@ -1,182 +0,0 @@
/**
* Built-in DSL Functions
*
* Native functions for use in DSL expressions
* Includes IP operations, time functions, string utilities
*/
// Use optimized fast versions for hot paths
import {
isIpInCidrFast,
isPrivateIpFast,
ipToIntFast,
isIPv4Fast
} from '../../utils/ip-utils-fast.js';
import {
isIPv6,
isLoopbackIp,
ipEquals,
getIpVersion
} from '../../utils/ip-utils.js';
/**
* Registry of built-in functions
*/
export const BUILT_IN_FUNCTIONS = {
// IP Address Functions - Using optimized fast versions
ip_in_cidr: {
params: ['ip', 'cidr'],
evaluate: (ip, cidr) => {
if (!ip || !cidr) return false;
return isIpInCidrFast(String(ip), String(cidr));
}
},
ip_equals: {
params: ['ip1', 'ip2'],
evaluate: (ip1, ip2) => {
return ipEquals(String(ip1), String(ip2));
}
},
ip_version: {
params: ['ip'],
evaluate: (ip) => {
return getIpVersion(String(ip));
}
},
ip_is_private: {
params: ['ip'],
evaluate: (ip) => {
if (!ip) return false;
return isPrivateIpFast(String(ip));
}
},
ip_is_loopback: {
params: ['ip'],
evaluate: (ip) => {
if (!ip) return false;
// Fast check: 127.x.x.x
return ipToIntFast(String(ip)) >>> 24 === 127;
}
},
ip_is_v4: {
params: ['ip'],
evaluate: (ip) => {
if (!ip) return false;
return isIPv4Fast(String(ip));
}
},
ip_is_v6: {
params: ['ip'],
evaluate: (ip) => {
if (!ip) return false;
return isIPv6(String(ip));
}
},
// Time Functions
hour_of_day: {
params: ['timestamp'],
evaluate: (timestamp) => {
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
return new Date(ts).getHours();
}
},
day_of_week: {
params: ['timestamp'],
evaluate: (timestamp) => {
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
return new Date(ts).getDay(); // 0 = Sunday
}
},
// String Functions
contains: {
params: ['string', 'substring'],
evaluate: (str, substr) => {
if (!str || !substr) return false;
return String(str).includes(String(substr));
}
},
starts_with: {
params: ['string', 'prefix'],
evaluate: (str, prefix) => {
if (!str || !prefix) return false;
return String(str).startsWith(String(prefix));
}
},
ends_with: {
params: ['string', 'suffix'],
evaluate: (str, suffix) => {
if (!str || !suffix) return false;
return String(str).endsWith(String(suffix));
}
},
// Comparison Functions
equals: {
params: ['a', 'b'],
evaluate: (a, b) => a === b
},
greater_than: {
params: ['a', 'b'],
evaluate: (a, b) => a > b
},
less_than: {
params: ['a', 'b'],
evaluate: (a, b) => a < b
},
in_range: {
params: ['value', 'min', 'max'],
evaluate: (value, min, max) => value >= min && value <= max
}
};
/**
* Check if a function name is a built-in
*/
export function isBuiltInFunction(name) {
return name in BUILT_IN_FUNCTIONS;
}
/**
* Evaluate a built-in function
*/
export function evaluateBuiltIn(name, args) {
const func = BUILT_IN_FUNCTIONS[name];
if (!func) {
throw new Error(`Unknown built-in function: ${name}`);
}
if (args.length !== func.params.length) {
throw new Error(
`Function ${name} expects ${func.params.length} arguments, got ${args.length}`
);
}
return func.evaluate(...args);
}
/**
* Get function signature
*/
export function getFunctionSignature(name) {
const func = BUILT_IN_FUNCTIONS[name];
if (!func) return null;
return {
name,
params: func.params
};
}
@@ -1,323 +0,0 @@
/**
* Expression Interpreter
*
* Interprets inline DSL expressions by evaluating them against existing
* compiled DSL rules in the graph. No temporary rules are created.
*/
import { PredicateResolver } from './PredicateResolver.js';
export class ExpressionInterpreter {
constructor(context, options = {}) {
this.context = context;
this.graphStores = context.graphStores;
this.resolver = new PredicateResolver(context);
this.customBuiltIns = options.customBuiltIns || {};
}
/**
* Interpret an expression AST against the graph
*
* @param {Object} ast - Parsed expression AST
* @param {Object} bindings - Variable bindings
* @returns {Object} Interpretation result
*/
async interpret(ast, bindings) {
switch (ast.type) {
case 'Fusion':
return this.interpretFusion(ast, bindings);
case 'Or':
return this.interpretOr(ast, bindings);
case 'And':
return this.interpretAnd(ast, bindings);
case 'Not':
return this.interpretNot(ast, bindings);
case 'Defeasible':
return this.interpretDefeasible(ast, bindings);
case 'Predicate':
return this.interpretPredicate(ast, bindings);
default:
throw new Error(`Unknown AST node type: ${ast.type}`);
}
}
/**
* Interpret FUSION block - ALL expressions must be true
* Uses OWA semantics: minimum possibility across all expressions
*/
async interpretFusion(ast, bindings) {
const results = await Promise.all(
ast.expressions.map(expr => this.interpret(expr, bindings))
);
const allAllowed = results.every(r => r.allowed);
const minPossibility = results.length > 0
? Math.min(...results.map(r => r.possibility || 0))
: 0;
return {
allowed: allAllowed,
possibility: minPossibility,
type: 'Fusion',
details: results
};
}
/**
* Interpret OR - ANY expression can be true (short-circuited)
*/
async interpretOr(ast, bindings) {
for (const operand of ast.operands) {
const result = await this.interpret(operand, bindings);
if (result.allowed) {
return {
allowed: true,
possibility: result.possibility,
type: 'Or',
satisfiedBy: operand
};
}
}
return {
allowed: false,
possibility: 0,
type: 'Or'
};
}
/**
* Interpret AND - ALL expressions must be true
*/
async interpretAnd(ast, bindings) {
const results = [];
let minPossibility = 1;
for (const operand of ast.operands) {
const result = await this.interpret(operand, bindings);
results.push(result);
minPossibility = Math.min(minPossibility, result.possibility || 0);
if (!result.allowed) {
return {
allowed: false,
possibility: 0,
type: 'And',
failedAt: operand,
details: results
};
}
}
return {
allowed: true,
possibility: minPossibility,
type: 'And',
details: results
};
}
/**
* Interpret NOT - negate the operand result
*/
async interpretNot(ast, bindings) {
const result = await this.interpret(ast.operand, bindings);
return {
allowed: !result.allowed,
possibility: result.allowed ? 0 : 1,
type: 'Not',
inner: result
};
}
/**
* Interpret Defeasible - primary UNLESS exception
* If exception is true, primary is defeated
*/
async interpretDefeasible(ast, bindings) {
// Check exception first (short-circuit if possible)
const exceptionResult = await this.interpret(ast.exception, bindings);
if (exceptionResult.allowed) {
return {
allowed: false,
possibility: 0,
type: 'Defeasible',
reason: 'Defeated by exception',
defeatedBy: exceptionResult
};
}
// Exception is false, evaluate primary
const primaryResult = await this.interpret(ast.primary, bindings);
return {
...primaryResult,
type: 'Defeasible',
primary: primaryResult,
exception: exceptionResult
};
}
/**
* Interpret Predicate - call existing DSL rule via graph.check()
* OR evaluate built-in function
*
* This is where we use the ALREADY COMPILED DSL rules.
* We do NOT create temporary rules.
*/
async interpretPredicate(ast, bindings) {
// Check for built-in functions first (ip_in_cidr, etc.)
const { isBuiltInFunction, evaluateBuiltIn } = await import('./BuiltInFunctions.js');
const resolvedArgs = ast.args.map(arg => this.resolveArgument(arg, bindings));
// Check custom built-ins first (e.g., PriceOps predicates)
if (this.customBuiltIns[ast.name]) {
const result = await this.customBuiltIns[ast.name](...resolvedArgs);
return {
allowed: result === true || result === 1,
possibility: result === true || result === 1 ? 1 : 0,
type: 'BuiltInFunction',
function: ast.name,
args: resolvedArgs,
result
};
}
if (isBuiltInFunction(ast.name)) {
const result = evaluateBuiltIn(ast.name, resolvedArgs);
return {
allowed: result === true || result === 1,
possibility: result === true || result === 1 ? 1 : 0,
type: 'BuiltInFunction',
function: ast.name,
args: resolvedArgs,
result
};
}
// Resolve predicate to existing rule
const rule = this.resolver.resolve(ast.name);
if (!rule) {
throw new Error(`Unknown predicate: ${ast.name} - must be defined in compiled DSL`);
}
// Extract subject (user) and optional object
const subject = resolvedArgs[0]; // First arg is always the subject
// For single-argument predicates, use subject as object to avoid "missing_node" errors
const object = resolvedArgs[1] || subject; // Second arg is optional object
// Get the appropriate graph store
const graphStore = this.context.getGraphStore
? this.context.getGraphStore(rule.scope, {
tenantId: bindings.tenant,
applicationId: bindings.applicationId
})
: this.graphStores[rule.scope];
if (!graphStore) {
throw new Error(`Graph store not found for scope: ${rule.scope}`);
}
// Execute check using EXISTING compiled rule
// The graphStore.check() will use the pre-compiled DSL rule config
const checkOptions = {};
if (bindings.partialGraph) {
checkOptions.partialGraph = bindings.partialGraph;
}
const result = graphStore.check(subject, ast.name, object, checkOptions);
return {
allowed: result?.allowed || result?.possibility === 1,
possibility: result?.possibility || 0,
type: 'Predicate',
predicate: ast.name,
scope: rule.scope,
subject,
object,
rawResult: result
};
}
/**
* Resolve an argument to its actual value
*/
resolveArgument(arg, bindings) {
switch (arg.type) {
case 'Variable':
return this.resolveVariable(arg, bindings);
case 'Reference':
return this.resolveReference(arg, bindings);
case 'Literal':
return arg.value;
default:
throw new Error(`Unknown argument type: ${arg.type}`);
}
}
/**
* Resolve a variable binding
* :user -> bindings.user
* :params.id -> bindings.params.id
*/
resolveVariable(variable, bindings) {
let value = bindings[variable.name];
// Handle nested paths: :params.id
if (variable.path && variable.path.length > 0) {
for (const key of variable.path) {
if (value === undefined || value === null) {
return undefined;
}
value = value[key];
}
}
return value;
}
/**
* Resolve a typed reference
* document::params.id -> bindings.params.id with type info
*/
resolveReference(ref, bindings) {
// Typed references like document::params.id
// The type (document) is metadata, the value comes from the path
let value = bindings;
for (const key of ref.path) {
if (value === undefined || value === null) {
return undefined;
}
value = value[key];
}
return value;
}
}
/**
* Utility to collect all predicates from an AST
* Used for validation before interpretation
*/
export function collectPredicates(ast, predicates = []) {
if (ast.type === 'Predicate') {
predicates.push(ast);
}
// Recursively collect from child nodes
const childKeys = ['expressions', 'operands', 'operand', 'primary', 'exception', 'inner'];
for (const key of childKeys) {
if (ast[key]) {
if (Array.isArray(ast[key])) {
ast[key].forEach(child => collectPredicates(child, predicates));
} else {
collectPredicates(ast[key], predicates);
}
}
}
return predicates;
}
-167
View File
@@ -1,167 +0,0 @@
/**
* Predicate Resolver
*
* Maps predicate names to existing compiled DSL rules across all graph scopes.
* Does NOT create new rules - only looks up existing ones.
*/
export class PredicateResolver {
constructor(context) {
this.context = context;
this.graphStores = context.graphStores || {};
this.cache = new Map();
}
/**
* Resolve a predicate name to its DSL rule
*
* @param {string} predicateName - Name of the predicate
* @returns {Object|null} Rule info or null if not found
*/
resolve(predicateName) {
// Check cache first
if (this.cache.has(predicateName)) {
return this.cache.get(predicateName);
}
// Look up in all graph scopes
const rule = this.findRule(predicateName);
if (rule) {
this.cache.set(predicateName, rule);
}
return rule;
}
/**
* Find a rule across all graph scopes
* Prefers logical rules over direct rules for evidence predicates
*/
findRule(predicateName) {
const scopes = [
'tenantExternal',
'tenantInternal',
'rootExternal',
'rootInternal',
'masterExternal',
'masterInternal'
];
let directRule = null;
let directScope = null;
for (const scopeName of scopes) {
const graphStore = this.graphStores[scopeName];
if (!graphStore) {
continue;
}
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
const arbiter = graphStore.arbiter || graphStore;
const config = arbiter.relationConfigs?.get(predicateName);
if (config) {
// Prefer logical rules (intersection/union) over direct rules
// This ensures evidence rules work correctly across all scopes
if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
return {
name: predicateName,
scope: scopeName,
config: config,
arity: this.inferArity(config)
};
}
// Remember the first direct rule as fallback
if (!directRule && config.type === 'direct') {
directRule = config;
directScope = scopeName;
}
}
}
// Return direct rule if no logical rule found
if (directRule) {
return {
name: predicateName,
scope: directScope,
config: directRule,
arity: this.inferArity(directRule)
};
}
return null;
}
/**
* Infer the arity (parameter count) from rule config
*/
inferArity(config) {
// Most DSL evidence rules have 1 or 2 parameters:
// - 1 param: just the subject (user)
// - 2 params: subject (user) + object
if (config.arity) {
return config.arity;
}
// Default to checking if it's a relation that typically needs an object
// This is a heuristic - in practice, the DSL defines this explicitly
if (config.type === 'tuple_to_userset' || config.type === 'direct') {
return 2; // Likely needs subject + object
}
return 1; // Default to 1 param
}
/**
* Check if a predicate exists without full resolution
*/
exists(predicateName) {
return this.resolve(predicateName) !== null;
}
/**
* Get all available predicates across all scopes
*/
getAllPredicates() {
const predicates = [];
const scopes = [
'tenantExternal',
'tenantInternal',
'rootExternal',
'rootInternal',
'masterExternal',
'masterInternal'
];
for (const scopeName of scopes) {
const graphStore = this.graphStores[scopeName];
if (!graphStore) {
continue;
}
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
const arbiter = graphStore.arbiter || graphStore;
if (arbiter.relationConfigs) {
for (const [name, config] of arbiter.relationConfigs) {
predicates.push({
name,
scope: scopeName,
arity: this.inferArity(config)
});
}
}
}
return predicates;
}
/**
* Clear the cache (useful for testing or when rules change)
*/
clearCache() {
this.cache.clear();
}
}
-162
View File
@@ -1,162 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for aggregation expressions
* Represents: aggregate { ... } USING majority
*/
export class AggregationNode extends BaseNode {
constructor(location = null) {
super('Aggregation', location);
this.expressions = []; // Array of expressions to aggregate
this.method = null; // Aggregation method ('majority', 'max', 'min', 'sum', 'avg')
this.weights = null; // Optional weights array
}
/**
* Add an expression to this aggregation
* @param {ExpressionNode} expression - Expression to add
*/
addExpression(expression) {
this.expressions.push(expression);
this.addChild(expression);
}
/**
* Set the aggregation method
* @param {string} method - Aggregation method
*/
setMethod(method) {
this.method = method;
}
/**
* Set weights for this aggregation
* @param {number[]} weights - Weights array
*/
setWeights(weights) {
this.weights = weights;
}
/**
* Get all expressions
* @returns {ExpressionNode[]} Expressions to aggregate
*/
getExpressions() {
return this.expressions;
}
/**
* Get the aggregation method
* @returns {string|null} Aggregation method or null
*/
getMethod() {
return this.method;
}
/**
* Get the weights for this aggregation
* @returns {number[]|null} Weights or null
*/
getWeights() {
return this.weights;
}
/**
* Check if this aggregation has weights
* @returns {boolean} True if has weights
*/
hasWeights() {
return this.weights !== null && this.weights.length > 0;
}
/**
* Check if this is a majority aggregation
* @returns {boolean} True if majority
*/
isMajority() {
return this.method === 'majority';
}
/**
* Check if this is a max aggregation
* @returns {boolean} True if max
*/
isMax() {
return this.method === 'max';
}
/**
* Check if this is a min aggregation
* @returns {boolean} True if min
*/
isMin() {
return this.method === 'min';
}
/**
* Check if this is a sum aggregation
* @returns {boolean} True if sum
*/
isSum() {
return this.method === 'sum';
}
/**
* Check if this is an average aggregation
* @returns {boolean} True if average
*/
isAverage() {
return this.method === 'avg';
}
/**
* Get the number of expressions
* @returns {number} Number of expressions
*/
getExpressionCount() {
return this.expressions.length;
}
/**
* Validate the aggregation
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate method
const validMethods = ['majority', 'max', 'min', 'sum', 'avg', 'count'];
if (!this.method || !validMethods.includes(this.method)) {
errors.push(`Invalid aggregation method: ${this.method}`);
}
// Validate expressions
if (this.expressions.length === 0) {
errors.push('Aggregation must have at least one expression');
}
// Validate each expression
this.expressions.forEach((expr, index) => {
const exprErrors = expr.validate ? expr.validate() : [];
errors.push(...exprErrors.map(err => `Expression ${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.expressions.length) {
errors.push('Weights array length must match expression count');
} else if (this.weights.some(w => typeof w !== 'number' || w < 0)) {
errors.push('All weights must be non-negative numbers');
}
}
return errors;
}
toString() {
const weightsStr = this.hasWeights() ? ` weights[${this.weights.length}]` : '';
return `Aggregation(${this.method}, ${this.expressions.length} expressions${weightsStr})`;
}
}
-157
View File
@@ -1,157 +0,0 @@
/**
* Base AST Node class for all DSL AST nodes
* Provides common functionality for all AST nodes
*/
export class BaseNode {
constructor(type, location = null) {
this.type = type;
this.location = location; // { start, end, line, column }
this.parent = null;
this.children = [];
}
/**
* Add a child node to this node
* @param {BaseNode} child - Child node to add
*/
addChild(child) {
if (child) {
child.parent = this;
this.children.push(child);
}
return this;
}
/**
* Add multiple child nodes
* @param {BaseNode[]} children - Array of child nodes
*/
addChildren(children) {
children.forEach(child => this.addChild(child));
return this;
}
/**
* Get all children of a specific type
* @param {string} type - Node type to filter by
* @returns {BaseNode[]} Filtered children
*/
getChildrenOfType(type) {
return this.children.filter(child => child.type === type);
}
/**
* Find the first child of a specific type
* @param {string} type - Node type to find
* @returns {BaseNode|null} First matching child or null
*/
getChildOfType(type) {
return this.children.find(child => child.type === type) || null;
}
/**
* Get all descendants of a specific type
* @param {string} type - Node type to find
* @returns {BaseNode[]} All matching descendants
*/
getDescendantsOfType(type) {
const results = [];
this.children.forEach(child => {
if (child.type === type) {
results.push(child);
}
results.push(...child.getDescendantsOfType(type));
});
return results;
}
/**
* Accept a visitor (visitor pattern)
* @param {Object} visitor - Visitor object with visit methods
* @returns {*} Result of visitor.visit{NodeType}(this)
*/
accept(visitor) {
const methodName = `visit${this.type}`;
if (visitor[methodName]) {
return visitor[methodName](this);
}
if (visitor.visit) {
return visitor.visit(this);
}
return null;
}
/**
* Get a string representation of this node
* @returns {string} String representation
*/
toString() {
return `${this.type}(${this.children.length} children)`;
}
/**
* Get a detailed string representation for debugging
* @returns {string} Detailed string representation
*/
toDebugString() {
const childrenStr = this.children.map(child =>
child.toDebugString ? child.toDebugString() : child.toString()
).join(', ');
return `${this.type}(${childrenStr})`;
}
/**
* Clone this node and all its children
* @returns {BaseNode} Cloned node
*/
clone() {
const cloned = new this.constructor();
cloned.type = this.type;
cloned.location = this.location ? { ...this.location } : null;
cloned.children = this.children.map(child => child.clone());
cloned.children.forEach(child => child.parent = cloned);
return cloned;
}
/**
* Get the root node of the AST
* @returns {BaseNode} Root node
*/
getRoot() {
let current = this;
while (current.parent) {
current = current.parent;
}
return current;
}
/**
* Get the depth of this node in the AST
* @returns {number} Depth from root
*/
getDepth() {
let depth = 0;
let current = this.parent;
while (current) {
depth++;
current = current.parent;
}
return depth;
}
/**
* Check if this node is a descendant of another node
* @param {BaseNode} ancestor - Potential ancestor node
* @returns {boolean} True if ancestor is an ancestor of this node
*/
isDescendantOf(ancestor) {
let current = this.parent;
while (current) {
if (current === ancestor) {
return true;
}
current = current.parent;
}
return false;
}
}
-151
View File
@@ -1,151 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for field behaviors (decay, blur, ttl)
* Represents: BEHAVES { decaying down hourly }
*/
export class BehaviorNode extends BaseNode {
constructor(type, location = null) {
super('Behavior', location);
this.type = type; // 'decay', 'blur', 'ttl'
this.parameters = new Map();
}
/**
* Set a parameter for this behavior
* @param {string} name - Parameter name
* @param {*} value - Parameter value
*/
setParameter(name, value) {
this.parameters.set(name, value);
}
/**
* Get a parameter value
* @param {string} name - Parameter name
* @returns {*} Parameter value or null
*/
getParameter(name) {
return this.parameters.get(name) || null;
}
/**
* Check if this is a decay behavior
* @returns {boolean} True if decay behavior
*/
isDecay() {
return this.type === 'decay';
}
/**
* Check if this is a blur behavior
* @returns {boolean} True if blur behavior
*/
isBlur() {
return this.type === 'blur';
}
/**
* Check if this is a TTL behavior
* @returns {boolean} True if TTL behavior
*/
isTTL() {
return this.type === 'ttl';
}
/**
* For decay behaviors, get the direction
* @returns {string|null} Decay direction or null
*/
getDecayDirection() {
return this.getParameter('direction');
}
/**
* For decay behaviors, get the period
* @returns {string|null} Decay period or null
*/
getDecayPeriod() {
return this.getParameter('period');
}
/**
* For blur behaviors, get the mode
* @returns {string|null} Blur mode or null
*/
getBlurMode() {
return this.getParameter('mode');
}
/**
* For blur behaviors, get the confidence level
* @returns {string|null} Confidence level or null
*/
getBlurConfidence() {
return this.getParameter('confidence');
}
/**
* For TTL behaviors, get the duration
* @returns {string|null} TTL duration or null
*/
getTTLDuration() {
return this.getParameter('duration');
}
/**
* Validate the behavior
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate behavior type
if (!['decay', 'blur', 'ttl'].includes(this.type)) {
errors.push(`Invalid behavior type: ${this.type}`);
}
// Validate decay behavior parameters
if (this.isDecay()) {
const direction = this.getDecayDirection();
if (!direction || !['up', 'down', 'neutral', 'stable'].includes(direction)) {
errors.push(`Invalid decay direction: ${direction}`);
}
const period = this.getDecayPeriod();
if (!period || !['hourly', 'daily', 'weekly', 'monthly'].includes(period)) {
errors.push(`Invalid decay period: ${period}`);
}
}
// Validate blur behavior parameters
if (this.isBlur()) {
const mode = this.getBlurMode();
if (!mode || !['fixed', 'adaptive', 'confidence'].includes(mode)) {
errors.push(`Invalid blur mode: ${mode}`);
}
const confidence = this.getBlurConfidence();
if (confidence && !['confidence_90', 'confidence_95', 'confidence_99'].includes(confidence)) {
errors.push(`Invalid blur confidence: ${confidence}`);
}
}
// Validate TTL behavior parameters
if (this.isTTL()) {
const duration = this.getTTLDuration();
if (!duration || !/^\d+[hd]$/.test(duration)) {
errors.push(`Invalid TTL duration: ${duration}`);
}
}
return errors;
}
toString() {
const params = Array.from(this.parameters.entries())
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
return `Behavior(${this.type}, ${params})`;
}
}
-150
View File
@@ -1,150 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for defeasible logic statements
* Represents: ALWAYS, WHEN, UNLESS, REQUIRES statements
*/
export class DefeasibleLogicNode extends BaseNode {
constructor(logicType, location = null) {
super('DefeasibleLogic', location);
this.logicType = logicType; // 'ALWAYS', 'WHEN', 'UNLESS', 'REQUIRES'
this.condition = null; // ExpressionNode or EvidenceBodyNode
this.defeater = null; // ExpressionNode or EvidenceBodyNode (for WHEN/UNLESS)
}
/**
* Set the condition for this defeasible logic
* @param {BaseNode} condition - Condition to set
*/
setCondition(condition) {
this.condition = condition;
this.addChild(condition);
}
/**
* Set the defeater for this defeasible logic (for WHEN/UNLESS)
* @param {BaseNode} defeater - Defeater to set
*/
setDefeater(defeater) {
this.defeater = defeater;
this.addChild(defeater);
}
/**
* Check if this is an ALWAYS statement
* @returns {boolean} True if ALWAYS
*/
isAlways() {
return this.logicType === 'ALWAYS';
}
/**
* Check if this is a WHEN statement
* @returns {boolean} True if WHEN
*/
isWhen() {
return this.logicType === 'WHEN';
}
/**
* Check if this is an UNLESS statement
* @returns {boolean} True if UNLESS
*/
isUnless() {
return this.logicType === 'UNLESS';
}
/**
* Check if this is a REQUIRES statement
* @returns {boolean} True if REQUIRES
*/
isRequires() {
return this.logicType === 'REQUIRES';
}
/**
* Check if this is a strict rule (ALWAYS)
* @returns {boolean} True if strict
*/
isStrict() {
return this.isAlways();
}
/**
* Check if this is a defeasible rule (WHEN)
* @returns {boolean} True if defeasible
*/
isDefeasible() {
return this.isWhen();
}
/**
* Check if this is a defeater (UNLESS)
* @returns {boolean} True if defeater
*/
isDefeater() {
return this.isUnless();
}
/**
* Check if this is a requirement (REQUIRES)
* @returns {boolean} True if requirement
*/
isRequirement() {
return this.isRequires();
}
/**
* Get the precedence level for this logic type
* @returns {number} Precedence level (higher = more important)
*/
getPrecedence() {
switch (this.logicType) {
case 'ALWAYS': return 3; // Highest precedence
case 'WHEN': return 2; // Medium precedence
case 'UNLESS': return 2; // Medium precedence
case 'REQUIRES': return 1; // Lowest precedence
default: return 0;
}
}
/**
* Validate the defeasible logic
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate logic type
if (!['ALWAYS', 'WHEN', 'UNLESS', 'REQUIRES'].includes(this.logicType)) {
errors.push(`Invalid logic type: ${this.logicType}`);
}
// Validate condition
if (!this.condition) {
errors.push(`${this.logicType} statement must have a condition`);
} else {
const condErrors = this.condition.validate ? this.condition.validate() : [];
errors.push(...condErrors);
}
// Validate defeater for WHEN/UNLESS
if ((this.isWhen() || this.isUnless()) && !this.defeater) {
errors.push(`${this.logicType} statement must have a defeater`);
}
// Validate defeater if present
if (this.defeater) {
const defErrors = this.defeater.validate ? this.defeater.validate() : [];
errors.push(...defErrors);
}
return errors;
}
toString() {
const condStr = this.condition ? this.condition.toString() : 'null';
const defStr = this.defeater ? ` UNLESS ${this.defeater.toString()}` : '';
return `DefeasibleLogic(${this.logicType} ${condStr}${defStr})`;
}
}
-117
View File
@@ -1,117 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for type definitions
* Represents: definition User { ... }
*/
export class DefinitionNode extends BaseNode {
constructor(name, definitionType = 'type', location = null) {
super('Definition', location);
this.name = name;
this.definitionType = definitionType; // 'type', 'interface', etc.
this.fields = [];
this.behaviors = new Map(); // field name -> behavior
this.cacheDirectives = new Map(); // field name -> cache directive
}
/**
* Add a field to the definition
* @param {FieldNode} field - Field to add
*/
addField(field) {
this.fields.push(field);
this.addChild(field);
}
/**
* Set behavior for a field
* @param {string} fieldName - Name of the field
* @param {BehaviorNode} behavior - Behavior to set
*/
setBehavior(fieldName, behavior) {
this.behaviors.set(fieldName, behavior);
}
/**
* Set cache directive for a field
* @param {string} fieldName - Name of the field
* @param {string} directive - Cache directive ('lazy')
*/
setCacheDirective(fieldName, directive) {
if (directive === 'eager') {
if (!DefinitionNode._warnedEagerCacheDirective) {
DefinitionNode._warnedEagerCacheDirective = true;
console.warn('[DefinitionNode] CACHE eager is deprecated; treating as CACHE lazy.');
}
this.cacheDirectives.set(fieldName, 'lazy');
return;
}
this.cacheDirectives.set(fieldName, directive);
}
/**
* Get behavior for a field
* @param {string} fieldName - Name of the field
* @returns {BehaviorNode|null} Behavior or null
*/
getBehavior(fieldName) {
return this.behaviors.get(fieldName) || null;
}
/**
* Get cache directive for a field
* @param {string} fieldName - Name of the field
* @returns {string|null} Cache directive or null
*/
getCacheDirective(fieldName) {
return this.cacheDirectives.get(fieldName) || null;
}
/**
* Find a field by name
* @param {string} fieldName - Name to search for
* @returns {FieldNode|null} Found field or null
*/
getField(fieldName) {
return this.fields.find(field => field.name === fieldName) || null;
}
/**
* Get all fields with a specific type
* @param {string} type - Type to filter by
* @returns {FieldNode[]} Filtered fields
*/
getFieldsOfType(type) {
return this.fields.filter(field => field.type === type);
}
/**
* Validate the definition
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Check for duplicate field names
const fieldNames = new Set();
this.fields.forEach(field => {
if (fieldNames.has(field.name)) {
errors.push(`Duplicate field name '${field.name}' in definition '${this.name}'`);
} else {
fieldNames.add(field.name);
}
});
// Validate each field
this.fields.forEach(field => {
const fieldErrors = field.validate ? field.validate() : [];
errors.push(...fieldErrors);
});
return errors;
}
toString() {
return `Definition(${this.name}: ${this.fields.length} fields)`;
}
}
-78
View File
@@ -1,78 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for direct evidence statements
* Represents: owns(user, doc)
*/
export class DirectEvidenceNode extends BaseNode {
constructor(location = null) {
super('DirectEvidence', location);
this.predicate = null; // PredicateNode
this.negated = false;
}
/**
* Set the predicate for this direct evidence
* @param {PredicateNode} predicate - Predicate to set
*/
setPredicate(predicate) {
this.predicate = predicate;
this.addChild(predicate);
}
/**
* Set whether this evidence is negated
* @param {boolean} negated - Whether evidence is negated
*/
setNegated(negated) {
this.negated = negated;
}
/**
* Check if this evidence is negated
* @returns {boolean} True if negated
*/
isNegated() {
return this.negated;
}
/**
* Get the predicate name
* @returns {string|null} Predicate name or null
*/
getPredicateName() {
return this.predicate ? this.predicate.name : null;
}
/**
* Get the predicate arguments
* @returns {ExpressionNode[]} Predicate arguments
*/
getArguments() {
return this.predicate ? this.predicate.arguments : [];
}
/**
* Validate the direct evidence
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate predicate
if (!this.predicate) {
errors.push('Direct evidence must have a predicate');
} else {
const predErrors = this.predicate.validate ? this.predicate.validate() : [];
errors.push(...predErrors);
}
return errors;
}
toString() {
const negStr = this.negated ? 'NOT ' : '';
const predStr = this.predicate ? this.predicate.toString() : 'null';
return `DirectEvidence(${negStr}${predStr})`;
}
}
-82
View File
@@ -1,82 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for evidence body containing statements
* Represents: { statement1; statement2; ... }
*/
export class EvidenceBodyNode extends BaseNode {
constructor(location = null) {
super('EvidenceBody', location);
this.statements = [];
}
/**
* Add a statement to the evidence body
* @param {BaseNode} statement - Statement to add
*/
addStatement(statement) {
this.statements.push(statement);
this.addChild(statement);
}
/**
* Get all statements of a specific type
* @param {string} type - Statement type to filter by
* @returns {BaseNode[]} Filtered statements
*/
getStatementsOfType(type) {
return this.statements.filter(stmt => stmt.type === type);
}
/**
* Get all direct evidence statements
* @returns {DirectEvidenceNode[]} Direct evidence statements
*/
getDirectEvidence() {
return this.getStatementsOfType('DirectEvidence');
}
/**
* Get all pattern matching statements
* @returns {PatternMatchNode[]} Pattern matching statements
*/
getPatternMatches() {
return this.getStatementsOfType('PatternMatch');
}
/**
* Get all defeasible logic statements
* @returns {DefeasibleLogicNode[]} Defeasible logic statements
*/
getDefeasibleLogic() {
return this.getStatementsOfType('DefeasibleLogic');
}
/**
* Get all fusion statements
* @returns {FusionNode[]} Fusion statements
*/
getFusions() {
return this.getStatementsOfType('Fusion');
}
/**
* Validate the evidence body
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate each statement
this.statements.forEach((stmt, index) => {
const stmtErrors = stmt.validate ? stmt.validate() : [];
errors.push(...stmtErrors.map(err => `Statement ${index + 1}: ${err}`));
});
return errors;
}
toString() {
return `EvidenceBody(${this.statements.length} statements)`;
}
}
-117
View File
@@ -1,117 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for evidence definitions
* Represents: evidence canRead(user: User, doc: Document) { ... } PROVIDES string
*/
export class EvidenceNode extends BaseNode {
constructor(name, location = null) {
super('Evidence', location);
this.name = name;
this.parameters = [];
this.returnType = null;
this.body = null; // EvidenceBodyNode
this.provides = null; // Return type specification
}
/**
* Add a parameter to the evidence
* @param {ParameterNode} parameter - Parameter to add
*/
addParameter(parameter) {
this.parameters.push(parameter);
this.addChild(parameter);
}
/**
* Set the body of the evidence
* @param {EvidenceBodyNode} body - Evidence body
*/
setBody(body) {
this.body = body;
this.addChild(body);
}
/**
* Set the return type for this evidence
* @param {string} returnType - Return type
*/
setReturnType(returnType) {
this.returnType = returnType;
this.provides = returnType;
}
/**
* Get the parameter names as an array
* @returns {string[]} Array of parameter names
*/
getParameterNames() {
return this.parameters.map(param => param.name);
}
/**
* Get the parameter types as an array
* @returns {string[]} Array of parameter types
*/
getParameterTypes() {
return this.parameters.map(param => param.type);
}
/**
* Find a parameter by name
* @param {string} name - Parameter name to find
* @returns {ParameterNode|null} Found parameter or null
*/
getParameter(name) {
return this.parameters.find(param => param.name === name) || null;
}
/**
* Get the signature string for this evidence
* @returns {string} Evidence signature
*/
getSignature() {
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
return `${this.name}(${paramStr})`;
}
/**
* Check if this evidence has a return type
* @returns {boolean} True if has return type
*/
hasReturnType() {
return this.returnType !== null;
}
/**
* Validate the evidence
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate evidence name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid evidence name: ${this.name}`);
}
// Validate parameters
this.parameters.forEach((param, index) => {
const paramErrors = param.validate ? param.validate() : [];
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
});
// Validate body
if (this.body) {
const bodyErrors = this.body.validate ? this.body.validate() : [];
errors.push(...bodyErrors);
}
return errors;
}
toString() {
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
return `Evidence(${this.getSignature()}${providesStr})`;
}
}
-260
View File
@@ -1,260 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for expressions (variables, literals, attribute access, etc.)
*/
export class ExpressionNode extends BaseNode {
constructor(expressionType, location = null) {
super('Expression', location);
this.expressionType = expressionType; // 'variable', 'literal', 'attribute', 'function', etc.
this.value = null;
this.name = null;
this.attribute = null;
this.object = null;
this.arguments = [];
this.operator = null;
this.left = null;
this.right = null;
}
/**
* Set the value for this expression
* @param {*} value - Value to set
*/
setValue(value) {
this.value = value;
}
/**
* Set the name for this expression
* @param {string} name - Name to set
*/
setName(name) {
this.name = name;
}
/**
* Set the attribute for this expression
* @param {string} attribute - Attribute to set
*/
setAttribute(attribute) {
this.attribute = attribute;
}
/**
* Set the object for this expression
* @param {ExpressionNode} object - Object to set
*/
setObject(object) {
this.object = object;
this.addChild(object);
}
/**
* Add an argument to this expression
* @param {ExpressionNode} argument - Argument to add
*/
addArgument(argument) {
this.arguments.push(argument);
this.addChild(argument);
}
/**
* Set the operator for this expression
* @param {string} operator - Operator to set
*/
setOperator(operator) {
this.operator = operator;
}
/**
* Set the left operand for this expression
* @param {ExpressionNode} left - Left operand to set
*/
setLeft(left) {
this.left = left;
this.addChild(left);
}
/**
* Set the right operand for this expression
* @param {ExpressionNode} right - Right operand to set
*/
setRight(right) {
this.right = right;
this.addChild(right);
}
/**
* Check if this is a variable expression
* @returns {boolean} True if variable
*/
isVariable() {
return this.expressionType === 'variable';
}
/**
* Check if this is a literal expression
* @returns {boolean} True if literal
*/
isLiteral() {
return this.expressionType === 'literal';
}
/**
* Check if this is an attribute access expression
* @returns {boolean} True if attribute access
*/
isAttributeAccess() {
return this.expressionType === 'attribute';
}
/**
* Check if this is a function call expression
* @returns {boolean} True if function call
*/
isFunctionCall() {
return this.expressionType === 'function';
}
/**
* Check if this is a binary operation expression
* @returns {boolean} True if binary operation
*/
isBinaryOperation() {
return this.expressionType === 'binary';
}
/**
* Check if this is a wildcard variable
* @returns {boolean} True if wildcard
*/
isWildcard() {
return this.isVariable() && this.name && this.name.startsWith('*');
}
/**
* Get the variable name (without wildcard prefix)
* @returns {string|null} Variable name or null
*/
getVariableName() {
if (this.isVariable() && this.name) {
return this.name.startsWith('*') ? this.name.substring(1) : this.name;
}
return null;
}
/**
* Get the full attribute path
* @returns {string|null} Full attribute path or null
*/
getAttributePath() {
if (this.isAttributeAccess()) {
const objStr = this.object ? this.object.toString() : '';
return `${objStr}.${this.attribute}`;
}
return null;
}
/**
* Get the function signature
* @returns {string|null} Function signature or null
*/
getFunctionSignature() {
if (this.isFunctionCall()) {
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
return `${this.name}(${argStr})`;
}
return null;
}
/**
* Validate the expression
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate expression type
const validTypes = ['variable', 'literal', 'attribute', 'function', 'binary'];
if (!validTypes.includes(this.expressionType)) {
errors.push(`Invalid expression type: ${this.expressionType}`);
}
// Validate variable expressions
if (this.isVariable() && !this.name) {
errors.push('Variable expression must have a name');
}
// Validate literal expressions
if (this.isLiteral() && this.value === null) {
errors.push('Literal expression must have a value');
}
// Validate attribute access expressions
if (this.isAttributeAccess()) {
if (!this.attribute) {
errors.push('Attribute access expression must have an attribute');
}
if (this.object) {
const objErrors = this.object.validate ? this.object.validate() : [];
errors.push(...objErrors);
}
}
// Validate function call expressions
if (this.isFunctionCall()) {
if (!this.name) {
errors.push('Function call expression must have a name');
}
this.arguments.forEach((arg, index) => {
const argErrors = arg.validate ? arg.validate() : [];
errors.push(...argErrors.map(err => `Argument ${index + 1}: ${err}`));
});
}
// Validate binary operation expressions
if (this.isBinaryOperation()) {
if (!this.operator) {
errors.push('Binary operation expression must have an operator');
}
if (!this.left) {
errors.push('Binary operation expression must have a left operand');
}
if (!this.right) {
errors.push('Binary operation expression must have a right operand');
}
if (this.left) {
const leftErrors = this.left.validate ? this.left.validate() : [];
errors.push(...leftErrors);
}
if (this.right) {
const rightErrors = this.right.validate ? this.right.validate() : [];
errors.push(...rightErrors);
}
}
return errors;
}
toString() {
switch (this.expressionType) {
case 'variable':
return `Variable(${this.name})`;
case 'literal':
return `Literal(${this.value})`;
case 'attribute':
const objStr = this.object ? this.object.toString() : '';
return `Attribute(${objStr}.${this.attribute})`;
case 'function':
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
return `Function(${this.name}(${argStr}))`;
case 'binary':
const leftStr = this.left ? this.left.toString() : 'null';
const rightStr = this.right ? this.right.toString() : 'null';
return `Binary(${leftStr} ${this.operator} ${rightStr})`;
default:
return `Expression(${this.expressionType})`;
}
}
}
-166
View File
@@ -1,166 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for fact definitions
* Represents: fact hasRole(user: User, role: string) CACHE lazy
*/
export class FactNode extends BaseNode {
constructor(name, location = null) {
super('Fact', location);
this.name = name;
this.parameters = [];
this.returnType = null;
this.properties = new Map(); // transitive, symmetrical, etc.
this.cacheDirective = null;
this.limit = null;
}
/**
* Add a parameter to the fact
* @param {ParameterNode} parameter - Parameter to add
*/
addParameter(parameter) {
this.parameters.push(parameter);
this.addChild(parameter);
}
/**
* Set the return type for this fact
* @param {string} returnType - Return type
*/
setReturnType(returnType) {
this.returnType = returnType;
}
/**
* Set a property for this fact
* @param {string} name - Property name
* @param {*} value - Property value
*/
setProperty(name, value) {
this.properties.set(name, value);
}
/**
* Get a property value
* @param {string} name - Property name
* @returns {*} Property value or null
*/
getProperty(name) {
return this.properties.get(name) || null;
}
/**
* Set the cache directive for this fact
* @param {string} directive - Cache directive ('lazy')
*/
setCacheDirective(directive) {
if (directive === 'eager') {
if (!FactNode._warnedEagerCacheDirective) {
FactNode._warnedEagerCacheDirective = true;
console.warn('[FactNode] CACHE eager is deprecated; treating as CACHE lazy.');
}
this.cacheDirective = 'lazy';
return;
}
this.cacheDirective = directive;
}
/**
* Set the limit for this fact
* @param {number} limit - Limit value
*/
setLimit(limit) {
this.limit = limit;
}
/**
* Check if this fact is transitive
* @returns {boolean} True if transitive
*/
isTransitive() {
return this.getProperty('transitive') === true;
}
/**
* Check if this fact is symmetrical
* @returns {boolean} True if symmetrical
*/
isSymmetrical() {
return this.getProperty('symmetrical') === true;
}
/**
* Get the parameter names as an array
* @returns {string[]} Array of parameter names
*/
getParameterNames() {
return this.parameters.map(param => param.name);
}
/**
* Get the parameter types as an array
* @returns {string[]} Array of parameter types
*/
getParameterTypes() {
return this.parameters.map(param => param.type);
}
/**
* Find a parameter by name
* @param {string} name - Parameter name to find
* @returns {ParameterNode|null} Found parameter or null
*/
getParameter(name) {
return this.parameters.find(param => param.name === name) || null;
}
/**
* Get the signature string for this fact
* @returns {string} Fact signature
*/
getSignature() {
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
return `${this.name}(${paramStr})`;
}
/**
* Validate the fact
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate fact name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid fact name: ${this.name}`);
}
// Validate parameters
this.parameters.forEach((param, index) => {
const paramErrors = param.validate ? param.validate() : [];
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
});
// Validate cache directive
if (this.cacheDirective && !['lazy'].includes(this.cacheDirective)) {
errors.push(`Invalid cache directive: ${this.cacheDirective}`);
}
// Validate limit
if (this.limit !== null && (typeof this.limit !== 'number' || this.limit < 0)) {
errors.push(`Invalid limit: ${this.limit}`);
}
return errors;
}
toString() {
const props = Array.from(this.properties.entries())
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
const cacheStr = this.cacheDirective ? ` CACHE ${this.cacheDirective}` : '';
const limitStr = this.limit ? ` LIMIT ${this.limit}` : '';
return `Fact(${this.getSignature()}${props ? `, ${props}` : ''}${cacheStr}${limitStr})`;
}
}
-141
View File
@@ -1,141 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for field definitions within type definitions
* Represents: fieldName: type BEHAVES { ... } CACHE lazy
*/
export class FieldNode extends BaseNode {
constructor(name, type, location = null) {
super('Field', location);
this.name = name;
this.type = type;
this.isArray = false;
this.behavior = null;
this.cacheDirective = null;
this.isOptional = false;
this.defaultValue = null;
}
/**
* Set the behavior for this field
* @param {BehaviorNode} behavior - Behavior to set
*/
setBehavior(behavior) {
this.behavior = behavior;
this.addChild(behavior);
}
/**
* Set the cache directive for this field
* @param {string} directive - Cache directive ('lazy')
*/
setCacheDirective(directive) {
if (directive === 'eager') {
if (!FieldNode._warnedEagerCacheDirective) {
FieldNode._warnedEagerCacheDirective = true;
console.warn('[FieldNode] CACHE eager is deprecated; treating as CACHE lazy.');
}
this.cacheDirective = 'lazy';
return;
}
this.cacheDirective = directive;
}
/**
* Mark this field as an array type
* @param {boolean} isArray - Whether this is an array
*/
setArray(isArray) {
this.isArray = isArray;
}
/**
* Set whether this field is optional
* @param {boolean} optional - Whether field is optional
*/
setOptional(optional) {
this.isOptional = optional;
}
/**
* Set default value for this field
* @param {*} value - Default value
*/
setDefaultValue(value) {
this.defaultValue = value;
}
/**
* Get the full type string including array notation
* @returns {string} Full type string
*/
getFullType() {
let typeStr = this.type;
if (this.isArray) {
typeStr += '[]';
}
if (this.isOptional) {
typeStr += '?';
}
return typeStr;
}
/**
* Check if this field has decay behavior
* @returns {boolean} True if field has decay behavior
*/
hasDecayBehavior() {
return this.behavior && this.behavior.type === 'decay';
}
/**
* Check if this field has blur behavior
* @returns {boolean} True if field has blur behavior
*/
hasBlurBehavior() {
return this.behavior && this.behavior.type === 'blur';
}
/**
* Check if this field has TTL behavior
* @returns {boolean} True if field has TTL behavior
*/
hasTTLBehavior() {
return this.behavior && this.behavior.type === 'ttl';
}
/**
* Validate the field
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate field name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid field name: ${this.name}`);
}
// Validate type
if (!this.type || typeof this.type !== 'string') {
errors.push(`Invalid field type: ${this.type}`);
}
// Validate behavior if present
if (this.behavior) {
const behaviorErrors = this.behavior.validate ? this.behavior.validate() : [];
errors.push(...behaviorErrors);
}
// Validate cache directive
if (this.cacheDirective && !['lazy'].includes(this.cacheDirective)) {
errors.push(`Invalid cache directive: ${this.cacheDirective}`);
}
return errors;
}
toString() {
return `Field(${this.name}: ${this.getFullType()})`;
}
}
-170
View File
@@ -1,170 +0,0 @@
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})`;
}
}
-130
View File
@@ -1,130 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for measure body containing expressions
* Represents: { user.role }
*/
export class MeasureBodyNode extends BaseNode {
constructor(location = null) {
super('MeasureBody', location);
this.expression = null; // ExpressionNode
this.fusion = null; // FusionNode (optional)
this.aggregation = null; // AggregationNode (optional)
}
/**
* Set the expression for this measure body
* @param {ExpressionNode} expression - Expression to set
*/
setExpression(expression) {
this.expression = expression;
this.addChild(expression);
}
/**
* Set the fusion for this measure body
* @param {FusionNode} fusion - Fusion to set
*/
setFusion(fusion) {
this.fusion = fusion;
this.addChild(fusion);
}
/**
* Set the aggregation for this measure body
* @param {AggregationNode} aggregation - Aggregation to set
*/
setAggregation(aggregation) {
this.aggregation = aggregation;
this.addChild(aggregation);
}
/**
* Get the expression
* @returns {ExpressionNode|null} Expression or null
*/
getExpression() {
return this.expression;
}
/**
* Get the fusion
* @returns {FusionNode|null} Fusion or null
*/
getFusion() {
return this.fusion;
}
/**
* Get the aggregation
* @returns {AggregationNode|null} Aggregation or null
*/
getAggregation() {
return this.aggregation;
}
/**
* Check if this measure body has an expression
* @returns {boolean} True if has expression
*/
hasExpression() {
return this.expression !== null;
}
/**
* Check if this measure body has fusion
* @returns {boolean} True if has fusion
*/
hasFusion() {
return this.fusion !== null;
}
/**
* Check if this measure body has aggregation
* @returns {boolean} True if has aggregation
*/
hasAggregation() {
return this.aggregation !== null;
}
/**
* Validate the measure body
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Must have at least one of expression, fusion, or aggregation
if (!this.expression && !this.fusion && !this.aggregation) {
errors.push('Measure body must have an expression, fusion, or aggregation');
}
// Validate expression if present
if (this.expression) {
const exprErrors = this.expression.validate ? this.expression.validate() : [];
errors.push(...exprErrors);
}
// Validate fusion if present
if (this.fusion) {
const fusionErrors = this.fusion.validate ? this.fusion.validate() : [];
errors.push(...fusionErrors);
}
// Validate aggregation if present
if (this.aggregation) {
const aggErrors = this.aggregation.validate ? this.aggregation.validate() : [];
errors.push(...aggErrors);
}
return errors;
}
toString() {
const parts = [];
if (this.expression) parts.push(this.expression.toString());
if (this.fusion) parts.push(this.fusion.toString());
if (this.aggregation) parts.push(this.aggregation.toString());
return `MeasureBody(${parts.join(', ')})`;
}
}
-117
View File
@@ -1,117 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for measure definitions
* Represents: measure userRole(user: User) { ... } PROVIDES string
*/
export class MeasureNode extends BaseNode {
constructor(name, location = null) {
super('Measure', location);
this.name = name;
this.parameters = [];
this.returnType = null;
this.body = null; // MeasureBodyNode
this.provides = null; // Return type specification
}
/**
* Add a parameter to the measure
* @param {ParameterNode} parameter - Parameter to add
*/
addParameter(parameter) {
this.parameters.push(parameter);
this.addChild(parameter);
}
/**
* Set the body of the measure
* @param {MeasureBodyNode} body - Measure body
*/
setBody(body) {
this.body = body;
this.addChild(body);
}
/**
* Set the return type for this measure
* @param {string} returnType - Return type
*/
setReturnType(returnType) {
this.returnType = returnType;
this.provides = returnType;
}
/**
* Get the parameter names as an array
* @returns {string[]} Array of parameter names
*/
getParameterNames() {
return this.parameters.map(param => param.name);
}
/**
* Get the parameter types as an array
* @returns {string[]} Array of parameter types
*/
getParameterTypes() {
return this.parameters.map(param => param.type);
}
/**
* Find a parameter by name
* @param {string} name - Parameter name to find
* @returns {ParameterNode|null} Found parameter or null
*/
getParameter(name) {
return this.parameters.find(param => param.name === name) || null;
}
/**
* Get the signature string for this measure
* @returns {string} Measure signature
*/
getSignature() {
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
return `${this.name}(${paramStr})`;
}
/**
* Check if this measure has a return type
* @returns {boolean} True if has return type
*/
hasReturnType() {
return this.returnType !== null;
}
/**
* Validate the measure
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate measure name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid measure name: ${this.name}`);
}
// Validate parameters
this.parameters.forEach((param, index) => {
const paramErrors = param.validate ? param.validate() : [];
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
});
// Validate body
if (this.body) {
const bodyErrors = this.body.validate ? this.body.validate() : [];
errors.push(...bodyErrors);
}
return errors;
}
toString() {
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
return `Measure(${this.getSignature()}${providesStr})`;
}
}
-79
View File
@@ -1,79 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for function/evidence parameters
* Represents: user: User, role: string
*/
export class ParameterNode extends BaseNode {
constructor(name, type, location = null) {
super('Parameter', location);
this.name = name;
this.type = type;
this.isOptional = false;
this.defaultValue = null;
this.isArray = false;
}
/**
* Set whether this parameter is optional
* @param {boolean} optional - Whether parameter is optional
*/
setOptional(optional) {
this.isOptional = optional;
}
/**
* Set default value for this parameter
* @param {*} value - Default value
*/
setDefaultValue(value) {
this.defaultValue = value;
}
/**
* Set whether this parameter is an array
* @param {boolean} isArray - Whether parameter is an array
*/
setArray(isArray) {
this.isArray = isArray;
}
/**
* Get the full type string including array notation
* @returns {string} Full type string
*/
getFullType() {
let typeStr = this.type;
if (this.isArray) {
typeStr += '[]';
}
if (this.isOptional) {
typeStr += '?';
}
return typeStr;
}
/**
* Validate the parameter
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate parameter name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid parameter name: ${this.name}`);
}
// Validate type
if (!this.type || typeof this.type !== 'string') {
errors.push(`Invalid parameter type: ${this.type}`);
}
return errors;
}
toString() {
return `Parameter(${this.name}: ${this.getFullType()})`;
}
}
-142
View File
@@ -1,142 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for pattern matching statements
* Represents: isMember(user, *group) { ... } limit 5
*/
export class PatternMatchNode extends BaseNode {
constructor(location = null) {
super('PatternMatch', location);
this.predicate = null; // PredicateNode
this.body = null; // EvidenceBodyNode
this.limit = null;
this.withClause = null; // WithClauseNode
this.negated = false;
}
/**
* Set the predicate for this pattern match
* @param {PredicateNode} predicate - Predicate to set
*/
setPredicate(predicate) {
this.predicate = predicate;
this.addChild(predicate);
}
/**
* Set the body of the pattern match
* @param {EvidenceBodyNode} body - Evidence body
*/
setBody(body) {
this.body = body;
this.addChild(body);
}
/**
* Set the limit for this pattern match
* @param {number} limit - Limit value
*/
setLimit(limit) {
this.limit = limit;
}
/**
* Set the with clause for this pattern match
* @param {WithClauseNode} withClause - With clause
*/
setWithClause(withClause) {
this.withClause = withClause;
this.addChild(withClause);
}
/**
* Set whether this pattern match is negated
* @param {boolean} negated - Whether pattern match is negated
*/
setNegated(negated) {
this.negated = negated;
}
/**
* Check if this pattern match is negated
* @returns {boolean} True if negated
*/
isNegated() {
return this.negated;
}
/**
* Get the predicate name
* @returns {string|null} Predicate name or null
*/
getPredicateName() {
return this.predicate ? this.predicate.name : null;
}
/**
* Get the predicate arguments
* @returns {ExpressionNode[]} Predicate arguments
*/
getArguments() {
return this.predicate ? this.predicate.arguments : [];
}
/**
* Check if this pattern match has a limit
* @returns {boolean} True if has limit
*/
hasLimit() {
return this.limit !== null;
}
/**
* Check if this pattern match has a with clause
* @returns {boolean} True if has with clause
*/
hasWithClause() {
return this.withClause !== null;
}
/**
* Validate the pattern match
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate predicate
if (!this.predicate) {
errors.push('Pattern match must have a predicate');
} else {
const predErrors = this.predicate.validate ? this.predicate.validate() : [];
errors.push(...predErrors);
}
// Validate body
if (this.body) {
const bodyErrors = this.body.validate ? this.body.validate() : [];
errors.push(...bodyErrors);
}
// Validate limit
if (this.limit !== null && (typeof this.limit !== 'number' || this.limit < 0)) {
errors.push(`Invalid limit: ${this.limit}`);
}
// Validate with clause
if (this.withClause) {
const withErrors = this.withClause.validate ? this.withClause.validate() : [];
errors.push(...withErrors);
}
return errors;
}
toString() {
const negStr = this.negated ? 'NOT ' : '';
const predStr = this.predicate ? this.predicate.toString() : 'null';
const limitStr = this.limit ? ` limit ${this.limit}` : '';
const withStr = this.withClause ? ` ${this.withClause.toString()}` : '';
return `PatternMatch(${negStr}${predStr}${limitStr}${withStr})`;
}
}
-106
View File
@@ -1,106 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for predicate calls
* Represents: hasRole(user, role), owns(user, doc)
*/
export class PredicateNode extends BaseNode {
constructor(name, location = null) {
super('Predicate', location);
this.name = name;
this.arguments = [];
}
/**
* Add an argument to this predicate
* @param {ExpressionNode} argument - Argument to add
*/
addArgument(argument) {
this.arguments.push(argument);
this.addChild(argument);
}
/**
* Get the predicate name
* @returns {string} Predicate name
*/
getName() {
return this.name;
}
/**
* Get all arguments
* @returns {ExpressionNode[]} Predicate arguments
*/
getArguments() {
return this.arguments;
}
/**
* Get the number of arguments
* @returns {number} Number of arguments
*/
getArgumentCount() {
return this.arguments.length;
}
/**
* Get an argument by index
* @param {number} index - Argument index
* @returns {ExpressionNode|null} Argument or null
*/
getArgument(index) {
return this.arguments[index] || null;
}
/**
* Check if this predicate has a specific number of arguments
* @param {number} count - Expected argument count
* @returns {boolean} True if has expected count
*/
hasArgumentCount(count) {
return this.arguments.length === count;
}
/**
* Check if this predicate has any arguments
* @returns {boolean} True if has arguments
*/
hasArguments() {
return this.arguments.length > 0;
}
/**
* Get the signature string for this predicate
* @returns {string} Predicate signature
*/
getSignature() {
const argStr = this.arguments.map(arg => arg.toString()).join(', ');
return `${this.name}(${argStr})`;
}
/**
* Validate the predicate
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate predicate name
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid predicate name: ${this.name}`);
}
// Validate arguments
this.arguments.forEach((arg, index) => {
const argErrors = arg.validate ? arg.validate() : [];
errors.push(...argErrors.map(err => `Argument ${index + 1}: ${err}`));
});
return errors;
}
toString() {
return `Predicate(${this.getSignature()})`;
}
}
-158
View File
@@ -1,158 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* Root node of the AST representing the entire DSL program
*/
export class ProgramNode extends BaseNode {
constructor(location = null) {
super('Program', location);
this.definitions = [];
this.facts = [];
this.evidence = [];
this.measures = [];
}
/**
* Add a definition to the program
* @param {DefinitionNode} definition - Definition to add
*/
addDefinition(definition) {
this.definitions.push(definition);
this.addChild(definition);
}
/**
* Add a fact to the program
* @param {FactNode} fact - Fact to add
*/
addFact(fact) {
this.facts.push(fact);
this.addChild(fact);
}
/**
* Add evidence to the program
* @param {EvidenceNode} evidence - Evidence to add
*/
addEvidence(evidence) {
this.evidence.push(evidence);
this.addChild(evidence);
}
/**
* Add a measure to the program
* @param {MeasureNode} measure - Measure to add
*/
addMeasure(measure) {
this.measures.push(measure);
this.addChild(measure);
}
/**
* Get all definitions of a specific type
* @param {string} type - Definition type to filter by
* @returns {DefinitionNode[]} Filtered definitions
*/
getDefinitionsOfType(type) {
return this.definitions.filter(def => def.definitionType === type);
}
/**
* Find a definition by name
* @param {string} name - Name to search for
* @returns {DefinitionNode|null} Found definition or null
*/
getDefinitionByName(name) {
return this.definitions.find(def => def.name === name) || null;
}
/**
* Find evidence by name
* @param {string} name - Name to search for
* @returns {EvidenceNode|null} Found evidence or null
*/
getEvidenceByName(name) {
return this.evidence.find(ev => ev.name === name) || null;
}
/**
* Find a fact by name
* @param {string} name - Name to search for
* @returns {FactNode|null} Found fact or null
*/
getFactByName(name) {
return this.facts.find(fact => fact.name === name) || null;
}
/**
* Find a measure by name
* @param {string} name - Name to search for
* @returns {MeasureNode|null} Found measure or null
*/
getMeasureByName(name) {
return this.measures.find(measure => measure.name === name) || null;
}
/**
* Get all symbols (definitions, facts, evidence, measures) by name
* @param {string} name - Name to search for
* @returns {BaseNode[]} All matching symbols
*/
getSymbolsByName(name) {
return [
...this.definitions.filter(def => def.name === name),
...this.facts.filter(fact => fact.name === name),
...this.evidence.filter(ev => ev.name === name),
...this.measures.filter(measure => measure.name === name)
];
}
/**
* Validate the program structure
* @returns {Object} Validation result with errors and warnings
*/
validate() {
const errors = [];
const warnings = [];
// Check for duplicate names
const allNames = new Map();
[...this.definitions, ...this.facts, ...this.evidence, ...this.measures].forEach(symbol => {
if (allNames.has(symbol.name)) {
errors.push(`Duplicate symbol name: ${symbol.name}`);
} else {
allNames.set(symbol.name, symbol);
}
});
// Validate each definition
this.definitions.forEach(def => {
const defErrors = def.validate ? def.validate() : [];
errors.push(...defErrors);
});
// Validate each fact
this.facts.forEach(fact => {
const factErrors = fact.validate ? fact.validate() : [];
errors.push(...factErrors);
});
// Validate each evidence
this.evidence.forEach(ev => {
const evErrors = ev.validate ? ev.validate() : [];
errors.push(...evErrors);
});
// Validate each measure
this.measures.forEach(measure => {
const measureErrors = measure.validate ? measure.validate() : [];
errors.push(...measureErrors);
});
return { errors, warnings, isValid: errors.length === 0 };
}
toString() {
return `Program(${this.definitions.length} definitions, ${this.facts.length} facts, ${this.evidence.length} evidence, ${this.measures.length} measures)`;
}
}
-87
View File
@@ -1,87 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for source definitions
* Represents: source *mfa(user: User) PROVIDES Proof within 10m
*
* Sources are injectable object/proof references that must be
* provided in the partial graph before authorization evaluation.
* They always carry a PROVIDES type and an optional freshness window.
*/
export class SourceNode extends BaseNode {
constructor(name, location = null) {
super('Source', location);
this.name = name;
this.injectable = false;
this.parameters = [];
this.returnType = null;
this.provides = null;
this.within = null;
this.cacheDirective = null;
}
addParameter(parameter) {
this.parameters.push(parameter);
this.addChild(parameter);
}
setReturnType(returnType) {
this.returnType = returnType;
this.provides = returnType;
}
setWithin(within) {
this.within = within;
}
setCacheDirective(directive) {
this.cacheDirective = directive;
}
setInjectable(value) {
this.injectable = !!value;
}
getParameterNames() {
return this.parameters.map(param => param.name);
}
getParameterTypes() {
return this.parameters.map(param => param.type);
}
getParameter(name) {
return this.parameters.find(param => param.name === name) || null;
}
getSignature() {
const paramStr = this.parameters.map(param => `${param.name}: ${param.type}`).join(', ');
return `${this.name}(${paramStr})`;
}
hasReturnType() {
return this.returnType !== null;
}
validate() {
const errors = [];
if (!this.name || typeof this.name !== 'string') {
errors.push(`Invalid source name: ${this.name}`);
}
this.parameters.forEach((param, index) => {
const paramErrors = param.validate ? param.validate() : [];
errors.push(...paramErrors.map(err => `Parameter ${index + 1}: ${err}`));
});
return errors;
}
toString() {
const injectableStr = this.injectable ? '*' : '';
const providesStr = this.returnType ? ` PROVIDES ${this.returnType}` : '';
const withinStr = this.within ? ` within ${this.within.value}` : '';
return `Source(${injectableStr}${this.getSignature()}${providesStr}${withinStr})`;
}
}
-154
View File
@@ -1,154 +0,0 @@
import { BaseNode } from './BaseNode.js';
/**
* AST node for with clauses in pattern matching
* Represents: with similarity > 0.7
*/
export class WithClauseNode extends BaseNode {
constructor(location = null) {
super('WithClause', location);
this.condition = null; // ExpressionNode
this.operator = null; // '>', '>=', '<', '<=', '==', '!='
this.value = null; // Literal value
}
/**
* Set the condition for this with clause
* @param {ExpressionNode} condition - Condition to set
*/
setCondition(condition) {
this.condition = condition;
this.addChild(condition);
}
/**
* Set the operator for this with clause
* @param {string} operator - Operator to set
*/
setOperator(operator) {
this.operator = operator;
}
/**
* Set the value for this with clause
* @param {*} value - Value to set
*/
setValue(value) {
this.value = value;
}
/**
* Get the condition expression
* @returns {ExpressionNode|null} Condition expression or null
*/
getCondition() {
return this.condition;
}
/**
* Get the operator
* @returns {string|null} Operator or null
*/
getOperator() {
return this.operator;
}
/**
* Get the value
* @returns {*} Value or null
*/
getValue() {
return this.value;
}
/**
* Check if this is a greater than comparison
* @returns {boolean} True if greater than
*/
isGreaterThan() {
return this.operator === '>';
}
/**
* Check if this is a greater than or equal comparison
* @returns {boolean} True if greater than or equal
*/
isGreaterThanOrEqual() {
return this.operator === '>=';
}
/**
* Check if this is a less than comparison
* @returns {boolean} True if less than
*/
isLessThan() {
return this.operator === '<';
}
/**
* Check if this is a less than or equal comparison
* @returns {boolean} True if less than or equal
*/
isLessThanOrEqual() {
return this.operator === '<=';
}
/**
* Check if this is an equality comparison
* @returns {boolean} True if equality
*/
isEqual() {
return this.operator === '==';
}
/**
* Check if this is a not equal comparison
* @returns {boolean} True if not equal
*/
isNotEqual() {
return this.operator === '!=';
}
/**
* Get the comparison string
* @returns {string} Comparison string
*/
getComparisonString() {
const condStr = this.condition ? this.condition.toString() : 'null';
const valStr = this.value !== null ? this.value.toString() : 'null';
return `${condStr} ${this.operator} ${valStr}`;
}
/**
* Validate the with clause
* @returns {string[]} Array of error messages
*/
validate() {
const errors = [];
// Validate condition
if (!this.condition) {
errors.push('With clause must have a condition');
} else {
const condErrors = this.condition.validate ? this.condition.validate() : [];
errors.push(...condErrors);
}
// Validate operator
const validOperators = ['>', '>=', '<', '<=', '==', '!='];
if (!this.operator || !validOperators.includes(this.operator)) {
errors.push(`Invalid operator: ${this.operator}`);
}
// Validate value
if (this.value === null) {
errors.push('With clause must have a value');
}
return errors;
}
toString() {
return `WithClause(${this.getComparisonString()})`;
}
}
-24
View File
@@ -1,24 +0,0 @@
/**
* AST Node exports
* Central export file for all AST node classes
*/
export { BaseNode } from './BaseNode.js';
export { ProgramNode } from './ProgramNode.js';
export { DefinitionNode } from './DefinitionNode.js';
export { FieldNode } from './FieldNode.js';
export { BehaviorNode } from './BehaviorNode.js';
export { FactNode } from './FactNode.js';
export { ParameterNode } from './ParameterNode.js';
export { EvidenceNode } from './EvidenceNode.js';
export { EvidenceBodyNode } from './EvidenceBodyNode.js';
export { DirectEvidenceNode } from './DirectEvidenceNode.js';
export { PatternMatchNode } from './PatternMatchNode.js';
export { DefeasibleLogicNode } from './DefeasibleLogicNode.js';
export { FusionNode } from './FusionNode.js';
export { PredicateNode } from './PredicateNode.js';
export { ExpressionNode } from './ExpressionNode.js';
export { WithClauseNode } from './WithClauseNode.js';
export { MeasureNode } from './MeasureNode.js';
export { MeasureBodyNode } from './MeasureBodyNode.js';
export { AggregationNode } from './AggregationNode.js';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-114
View File
@@ -1,114 +0,0 @@
import * as GeneratedParser from './GeneratedParser.js';
/**
* Peggy-based DSL Parser
* Uses the generated parser from Peggy grammar
*/
export class PeggyDSLParser {
constructor() {
this.parser = GeneratedParser;
this.errors = [];
}
/**
* Parse DSL text into AST
* @param {string} dslText - DSL text to parse
* @returns {ProgramNode} Parsed AST
*/
parse(dslText) {
this.errors = [];
try {
const program = this.parser.parse(dslText);
return program;
} catch (error) {
this.errors.push(`Parse error: ${error.message}`);
// If the error has location information, add it to the error
if (error.location) {
const location = error.location;
this.errors.push(`Location: line ${location.start.line}, column ${location.start.column}`);
}
// If the error has expected/found information, add it
if (error.expected && error.found) {
this.errors.push(`Expected: ${error.expected.join(', ')}`);
this.errors.push(`Found: ${error.found}`);
}
throw new Error(`Parsing failed: ${this.errors.join('; ')}`);
}
}
/**
* Get parser errors from last parse
* @returns {string[]} Array of parser errors
*/
getErrors() {
return this.errors;
}
/**
* Validate DSL text without throwing errors
* @param {string} dslText - DSL text to validate
* @returns {Object} Validation result with success status and errors
*/
validate(dslText) {
try {
const program = this.parse(dslText);
return {
success: true,
errors: [],
program: program
};
} catch (error) {
return {
success: false,
errors: this.errors,
program: null
};
}
}
/**
* Parse with options
* @param {string} dslText - DSL text to parse
* @param {Object} options - Parser options
* @returns {ProgramNode} Parsed AST
*/
parseWithOptions(dslText, options = {}) {
this.errors = [];
try {
const program = this.parser.parse(dslText, options);
return program;
} catch (error) {
this.errors.push(`Parse error: ${error.message}`);
if (error.location) {
const location = error.location;
this.errors.push(`Location: line ${location.start.line}, column ${location.start.column}`);
}
if (error.expected && error.found) {
this.errors.push(`Expected: ${error.expected.join(', ')}`);
this.errors.push(`Found: ${error.found}`);
}
throw new Error(`Parsing failed: ${this.errors.join('; ')}`);
}
}
/**
* Get parser information
* @returns {Object} Parser information
*/
getParserInfo() {
return {
name: 'PeggyDSLParser',
version: '1.0.0',
generated: true,
grammar: 'dsl.peggy'
};
}
}
-45
View File
@@ -1,45 +0,0 @@
// Wrapper for Peggy-generated parser that handles ESM imports correctly
import {
ProgramNode, DefinitionNode, FieldNode, BehaviorNode, FactNode, ParameterNode,
EvidenceNode, EvidenceBodyNode, DirectEvidenceNode, PatternMatchNode,
DefeasibleLogicNode, FusionNode, PredicateNode, ExpressionNode, WithClauseNode,
MeasureNode, MeasureBodyNode, AggregationNode
} from '../nodes/index.js';
// Make AST nodes globally available to the generated parser
global.ProgramNode = ProgramNode;
global.DefinitionNode = DefinitionNode;
global.FieldNode = FieldNode;
global.BehaviorNode = BehaviorNode;
global.FactNode = FactNode;
global.ParameterNode = ParameterNode;
global.EvidenceNode = EvidenceNode;
global.EvidenceBodyNode = EvidenceBodyNode;
global.DirectEvidenceNode = DirectEvidenceNode;
global.PatternMatchNode = PatternMatchNode;
global.DefeasibleLogicNode = DefeasibleLogicNode;
global.FusionNode = FusionNode;
global.PredicateNode = PredicateNode;
global.ExpressionNode = ExpressionNode;
global.WithClauseNode = WithClauseNode;
global.MeasureNode = MeasureNode;
global.MeasureBodyNode = MeasureBodyNode;
global.AggregationNode = AggregationNode;
// Import the generated parser
import { parse } from './GeneratedParser.js';
// Create a wrapper class that matches the expected interface
export class PeggyDSLParser {
static parse(text) {
console.log('PeggyDSLParser: Parsing text:', text.substring(0, 100) + '...');
try {
const result = parse(text);
console.log('PeggyDSLParser: Parse result:', result);
return result;
} catch (error) {
console.error('PeggyDSLParser: Parse error:', error.message);
throw error;
}
}
}
-37
View File
@@ -1,37 +0,0 @@
export const DSL_PRELUDE = `
// Built-in types and relations available in every graph.
// These are intended for request-scoped auth/session evidence (partial graph inputs).
definition User {
id: string
}
definition Account {
id: string
tier: string
}
definition Device {
id: string
device_risk: number
auth_method: string
ip_address: string
user_agent: string
}
definition AuthSession {
login_time: timestamp
last_login_time: timestamp
mfa_used: boolean
auth_method: string
ip_address: string
user_agent: string
expires_at: timestamp
device_risk: number
}
fact session_for_user(user: User, session: AuthSession)
fact session_for_account(account: Account, session: AuthSession)
fact session_for_device(device: Device, session: AuthSession)
fact logged_in_as(device: Device, account: Account)
`;
File diff suppressed because it is too large Load Diff
-165
View File
@@ -1,165 +0,0 @@
/**
* Optimized IP Address Utilities
*
* High-performance versions for hot paths
*/
// CIDR cache for repeated lookups
const cidrCache = new Map();
const CIDR_CACHE_SIZE = 1000;
/**
* Fast IPv4 check - less strict but much faster
* Only validates format, not strict numeric ranges
*/
export function isIPv4Fast(ip) {
if (typeof ip !== 'string') return false;
// Quick length check (min: 7 for "0.0.0.0", max: 15 for "255.255.255.255")
if (ip.length < 7 || ip.length > 15) return false;
let dots = 0;
for (let i = 0; i < ip.length; i++) {
const c = ip.charCodeAt(i);
if (c === 46) { // '.'
dots++;
} else if (c < 48 || c > 57) { // not 0-9
return false;
}
}
return dots === 3;
}
/**
* Ultra-fast IP to integer conversion
* Direct character parsing, no string splitting
*/
export function ipToIntFast(ip) {
let result = 0;
let octet = 0;
let shift = 24;
for (let i = 0; i < ip.length; i++) {
const c = ip.charCodeAt(i);
if (c === 46) { // '.'
result |= (octet << shift);
octet = 0;
shift -= 8;
} else {
octet = octet * 10 + (c - 48);
}
}
return (result | octet) >>> 0;
}
/**
* Fast CIDR parsing with caching
*/
export function parseCidrCached(cidr) {
// Check cache first
let cached = cidrCache.get(cidr);
if (cached) return cached;
// Parse and cache
const slashIdx = cidr.indexOf('/');
if (slashIdx === -1) return null;
const ip = cidr.slice(0, slashIdx);
const prefix = parseInt(cidr.slice(slashIdx + 1), 10);
const mask = -1 << (32 - prefix);
cached = {
network: ipToIntFast(ip),
mask,
prefix
};
// Simple LRU - clear if too big
if (cidrCache.size >= CIDR_CACHE_SIZE) {
cidrCache.clear();
}
cidrCache.set(cidr, cached);
return cached;
}
/**
* Ultra-fast IP in CIDR check
* Uses caching and optimized parsing
*/
export function isIpInCidrFast(ip, cidr) {
const cached = parseCidrCached(cidr);
if (!cached) return false;
const ipInt = ipToIntFast(ip);
return (ipInt & cached.mask) === (cached.network & cached.mask);
}
/**
* Fast private IP check using bit manipulation
*/
export function isPrivateIpFast(ip) {
const ipInt = ipToIntFast(ip);
// 10.0.0.0/8: 0x0A000000 to 0x0AFFFFFF
if ((ipInt >>> 24) === 10) return true;
// 172.16.0.0/12: 0xAC100000 to 0xAC1FFFFF
const high16 = ipInt >>> 16;
if (high16 >= 0xAC10 && high16 <= 0xAC1F) return true;
// 192.168.0.0/16: 0xC0A80000 to 0xC0A8FFFF
if (high16 === 0xC0A8) return true;
// 127.0.0.0/8: 0x7F000000 to 0x7FFFFFFF
if ((ipInt >>> 24) === 127) return true;
// 169.254.0.0/16: 0xA9FE0000 to 0xA9FEFFFF
if (high16 === 0xA9FE) return true;
return false;
}
/**
* Optimized built-in function evaluator
* Direct dispatch without object lookups
*/
export function evaluateBuiltInFast(name, args) {
switch (name) {
case 'ip_in_cidr':
return isIpInCidrFast(args[0], args[1]);
case 'ip_is_private':
return isPrivateIpFast(args[0]);
case 'ip_is_loopback':
return (ipToIntFast(args[0]) >>> 24) === 127;
case 'ip_version':
return isIPv4Fast(args[0]) ? 4 : (args[0].includes(':') ? 6 : null);
case 'ip_equals':
return args[0] === args[1];
case 'contains':
return String(args[0]).includes(String(args[1]));
case 'starts_with':
return String(args[0]).startsWith(String(args[1]));
case 'ends_with':
return String(args[0]).endsWith(String(args[1]));
case 'equals':
return args[0] === args[1];
case 'greater_than':
return args[0] > args[1];
case 'less_than':
return args[0] < args[1];
case 'in_range':
return args[0] >= args[1] && args[0] <= args[2];
case 'hour_of_day':
return new Date(args[0]).getHours();
case 'day_of_week':
return new Date(args[0]).getDay();
default:
throw new Error(`Unknown: ${name}`);
}
}
// Re-export original functions for compatibility
export { isIPv4, isIPv6, isLoopbackIp, getIpVersion, normalizeIp } from './ip-utils.js';
export { isIpInCidr, isPrivateIp } from './ip-utils.js';
-123
View File
@@ -1,123 +0,0 @@
/**
* IP Address Utilities
*/
import { createRequire } from 'node:module';
/**
* Check if string is IPv4
*/
export function isIPv4(ip) {
if (typeof ip !== 'string') return false;
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every(part => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255 && part === String(num);
});
}
/**
* Check if string is IPv6
*/
export function isIPv6(ip) {
if (typeof ip !== 'string') return false;
// Simple check - contains colons and valid hex
return ip.includes(':') && /^[0-9a-fA-F:]+$/.test(ip);
}
/**
* Check if IP is in private range (RFC 1918)
*/
export function isPrivateIp(ip) {
if (!isIPv4(ip)) return false;
const parts = ip.split('.').map(Number);
const [a, b, c, d] = parts;
// 10.0.0.0/8
if (a === 10) return true;
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true;
// 192.168.0.0/16
if (a === 192 && b === 168) return true;
// 127.0.0.0/8 (loopback)
if (a === 127) return true;
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254) return true;
return false;
}
/**
* Check if IP is loopback
*/
export function isLoopbackIp(ip) {
if (!isIPv4(ip)) return false;
return ip.startsWith('127.');
}
/**
* Convert IP to integer for range comparison
*/
export function ipToInt(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}
/**
* Parse CIDR notation
*/
export function parseCidr(cidr) {
const [ip, prefix] = cidr.split('/');
const mask = -1 << (32 - parseInt(prefix, 10));
return { ip: ipToInt(ip), mask };
}
/**
* Check if IP is in CIDR range
*/
export function isIpInCidr(ip, cidr) {
if (!isIPv4(ip)) return false;
try {
const ipInt = ipToInt(ip);
const { ip: networkInt, mask } = parseCidr(cidr);
return (ipInt & mask) === (networkInt & mask);
} catch (err) {
return false;
}
}
/**
* Check if two IPs are equal
*/
export function ipEquals(ip1, ip2) {
return ip1 === ip2;
}
/**
* Get IP version (4 or 6)
*/
export function getIpVersion(ip) {
if (isIPv4(ip)) return 4;
if (isIPv6(ip)) return 6;
return null;
}
/**
* Normalize IP (remove leading zeros, etc.)
*/
export function normalizeIp(ip) {
if (!isIPv4(ip)) return ip;
return ip
.split('.')
.map(part => parseInt(part, 10).toString())
.join('.');
}
-265
View File
@@ -1,265 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
import { parse } from '../../src/ast/parser/GeneratedParser.js';
import { RuleGenerator } from '../../src/ast/generator/RuleGenerator.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('DSL Compiler', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic parsing', () => {
const dsl = `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string)
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
`;
const result = compiler.compile(dsl, 'test-basic');
assert.ok(result.success, 'Basic parsing should succeed');
assert.ok(result.program !== null, 'Program should be created');
assert.ok(result.generatedRules.size > 0, 'Rules should be generated');
});
test('Basic compilation', () => {
const dsl = `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string) CACHE eager
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
`;
const result = compiler.compile(dsl, 'test-compilation');
assert.ok(result.success, 'Basic compilation should succeed');
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
const canReadConfig = result.generatedRules.get('canRead');
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
assert.ok(canReadConfig.relation === 'hasRole', 'canRead should use hasRole relation');
});
test('Complex DSL compilation', () => {
const dsl = `
definition Employee {
role: string
isActive: boolean
clearance: string BEHAVES {
blurring adaptive confidence_95
} CACHE eager
}
fact hasRole(user: Employee, role: string) CACHE eager
fact isMember(user: Employee, group: Device) transitive CACHE lazy
fact owns(user: Employee, doc: Account) CACHE eager
evidence canRead(user: Employee, doc: Account) {
owns(user, doc)
hasRole(user, 'admin')
}
evidence canAccessCritical(user: Employee, resource: AuthSession) {
fusion min {
hasRole(user, 'admin'),
hasRole(user, 'superadmin')
}
fusion max {
hasRole(user, 'admin'),
hasRole(user, 'secret')
}
}
`;
const result = compiler.compile(dsl, 'test-complex');
assert.ok(result.success, 'Complex DSL compilation should succeed');
assert.ok(result.generatedRules.has('canRead'), 'canRead rule should be generated');
assert.ok(result.generatedRules.has('canAccessCritical'), 'canAccessCritical rule should be generated');
const canReadConfig = result.generatedRules.get('canRead');
assert.ok(canReadConfig.type === 'logical', 'canRead should be logical rule');
});
test('Error handling', () => {
const invalidDSL = `
definition Employee {
role: string
// Missing closing brace
fact hasRole(user: Employee, role: string)
// Missing semicolon
evidence canRead(user: Employee, doc: Account) {
// Invalid syntax
invalid syntax here
}
`;
const result = compiler.compile(invalidDSL, 'test-error');
assert.ok(!result.success, 'Invalid DSL should fail');
assert.ok(result.errors.length > 0, 'Should have error messages');
});
test('Program management', () => {
const dsl1 = `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string)
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
`;
const dsl2 = `
definition Employee {
role: string
isActive: boolean
}
fact hasBalance(user: Employee, amount: number)
evidence canWithdraw(user: Employee, amount: number) {
hasBalance(user, amount)
}
`;
const result1 = compiler.compile(dsl1, 'test-auth');
assert.ok(result1.success, 'First program should compile');
const programs = { 'auth': dsl1, 'finance': dsl2 };
const result2 = compiler.compileMultiple(programs);
assert.ok(result2.success, 'Multiple programs should compile');
const authProgram = compiler.getCompiledProgram('test-auth');
assert.ok(authProgram !== null, 'Should retrieve compiled program');
const removed = compiler.removeCompiledProgram('test-auth');
assert.ok(removed, 'Should remove program');
compiler.clearCompiledPrograms();
const allPrograms = compiler.getAllCompiledPrograms();
assert.ok(allPrograms.size === 0, 'Should clear all programs');
});
test('Validation', () => {
const validDSL = `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string)
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
`;
const invalidDSL = `
definition Employee {
role: string
// Missing closing brace
fact hasRole(user: Employee, role: string)
// Missing semicolon
`;
const validResult = compiler.validate(validDSL);
assert.ok(validResult.success, 'Valid DSL should pass validation');
const invalidResult = compiler.validate(invalidDSL);
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
});
test('Rule generation', () => {
const dsl = `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string) CACHE eager
fact isMember(user: Employee, group: Device) transitive CACHE lazy
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
evidence canAccess(user: Employee, doc: Account) {
hasRole(user, 'reader')
}
`;
const result = compiler.compile(dsl, 'test-rules');
assert.ok(result.success, 'Rule generation should succeed');
const canReadConfig = result.generatedRules.get('canRead');
assert.ok(canReadConfig.type === 'direct', 'canRead should be direct rule');
const canAccessConfig = result.generatedRules.get('canAccess');
assert.ok(canAccessConfig.type === 'direct', 'canAccess should be direct rule');
});
test('Multiple programs', () => {
const programs = {
'auth': `
definition Employee {
role: string
isActive: boolean
}
fact hasRole(user: Employee, role: string)
evidence canRead(user: Employee, doc: Account) {
hasRole(user, 'admin')
}
`,
'finance': `
definition Employee {
role: string
isActive: boolean
}
fact hasBalance(user: Employee, amount: number)
evidence canWithdraw(user: Employee, amount: number) {
hasBalance(user, amount)
}
`,
'invalid': `
// Invalid syntax
invalid syntax here
`
};
const result = compiler.compileMultiple(programs);
assert.ok(!result.success, 'Should fail due to invalid program');
assert.ok(result.errors.length > 0, 'Should have errors');
assert.ok(result.results.auth.success, 'Auth program should succeed');
assert.ok(result.results.finance.success, 'Finance program should succeed');
assert.ok(!result.results.invalid.success, 'Invalid program should fail');
});
});
-283
View File
@@ -1,283 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Type Definitions', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic definitions', () => {
const testCases = [
{
input: `definition User { role: string }`,
description: 'Simple definition with one field'
},
{
input: `definition User {
role: string
isActive: boolean
}`,
description: 'Definition with multiple fields'
},
{
input: `definition Group {
name: string
description: string
created: timestamp
}`,
description: 'Definition with different field types'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-basic-def-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
});
});
test('Field types', () => {
const testCases = [
{ type: 'string', description: 'String field type' },
{ type: 'number', description: 'Number field type' },
{ type: 'boolean', description: 'Boolean field type' },
{ type: 'timestamp', description: 'Timestamp field type' },
{ type: 'User', description: 'Custom type field' },
{ type: 'Permission', description: 'Another custom type field' }
];
testCases.forEach(({ type, description }) => {
const dsl = `definition Test { field: ${type} }`;
const result = compiler.compile(dsl, `test-field-type-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Array types', () => {
const testCases = [
{ type: 'string[]', description: 'String array' },
{ type: 'number[]', description: 'Number array' },
{ type: 'boolean[]', description: 'Boolean array' },
{ type: 'Permission[]', description: 'Custom type array' },
{ type: 'User[]', description: 'User array' }
];
testCases.forEach(({ type, description }) => {
const dsl = `definition Test { items: ${type} }`;
const result = compiler.compile(dsl, `test-array-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Behaviors', () => {
const testCases = [
{
input: `definition User {
balance: number BEHAVES { decaying down hourly }
}`,
description: 'Decay behavior - down hourly'
},
{
input: `definition User {
reputation: number BEHAVES { decaying up daily }
}`,
description: 'Decay behavior - up daily'
},
{
input: `definition User {
score: number BEHAVES { decaying neutral weekly }
}`,
description: 'Decay behavior - neutral weekly'
},
{
input: `definition User {
stability: number BEHAVES { decaying stable monthly }
}`,
description: 'Decay behavior - stable monthly'
},
{
input: `definition User {
confidence: number BEHAVES { blurring fixed }
}`,
description: 'Blur behavior - fixed'
},
{
input: `definition User {
accuracy: number BEHAVES { blurring adaptive }
}`,
description: 'Blur behavior - adaptive'
},
{
input: `definition User {
precision: number BEHAVES { blurring confidence confidence_90 }
}`,
description: 'Blur behavior - confidence with level'
},
{
input: `definition User {
session: string BEHAVES { ttl 1h }
}`,
description: 'TTL behavior - hours'
},
{
input: `definition User {
token: string BEHAVES { ttl 24h }
}`,
description: 'TTL behavior - 24 hours'
},
{
input: `definition User {
cache: string BEHAVES { ttl 7d }
}`,
description: 'TTL behavior - days'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-behavior-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Caching', () => {
const testCases = [
{
input: `definition User {
role: string CACHE eager
}`,
description: 'Eager caching'
},
{
input: `definition User {
score: number CACHE lazy
}`,
description: 'Lazy caching'
},
{
input: `definition User {
balance: number BEHAVES { decaying down hourly } CACHE eager
}`,
description: 'Behavior with eager caching'
},
{
input: `definition User {
reputation: number BEHAVES { blurring adaptive } CACHE lazy
}`,
description: 'Behavior with lazy caching'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-cache-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex definitions', () => {
const testCases = [
{
input: `definition User {
role: string
isActive: boolean
lastActive: timestamp BEHAVES {
decaying down hourly
} CACHE lazy
isSuspended: boolean
balance: number BEHAVES {
decaying down hourly
} CACHE eager
score: number BEHAVES {
blurring adaptive confidence_95
} CACHE lazy
session: string BEHAVES {
ttl 24h
} CACHE eager
}`,
description: 'Complex definition with multiple behaviors and caching'
},
{
input: `definition Group {
name: string
permissions: Permission[]
members: User[]
created: timestamp BEHAVES {
decaying stable monthly
} CACHE lazy
isPublic: boolean CACHE eager
}`,
description: 'Definition with arrays and mixed behaviors'
},
{
input: `definition Document {
level: string
owner: User
tags: string[]
content: string BEHAVES {
blurring fixed
} CACHE lazy
accessCount: number BEHAVES {
decaying up daily
} CACHE eager
expiresAt: timestamp BEHAVES {
ttl 30d
} CACHE eager
}`,
description: 'Definition with all behavior types'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-complex-def-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.definitions.length > 0, 'Should have definitions');
});
});
test('Definition error handling', () => {
const testCases = [
{
input: `definition User { role: string`,
description: 'Missing closing brace should fail'
},
{
input: `definition User { role: }`,
description: 'Missing field type should fail'
},
{
input: `definition User { : string }`,
description: 'Missing field name should fail'
},
{
input: `definition User { role: string BEHAVES { }`,
description: 'Incomplete behavior should fail'
},
{
input: `definition User { role: string CACHE }`,
description: 'Incomplete cache directive should fail'
},
{
input: `definition User { role: string BEHAVES { invalid } }`,
description: 'Invalid behavior should fail'
}
];
testCases.forEach(({ input, description }) => {
try {
const result = compiler.compile(input, `test-def-error-${Date.now()}`);
assert.ok(!result.success, `${description} should fail to parse`);
} catch {
// Expected to fail
}
});
});
});
-374
View File
@@ -1,374 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Evidence Rules', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic evidence', () => {
const testCases = [
{
input: `evidence canRead(user: User, doc: Document) {
hasRole(user, 'admin')
}`,
description: 'Simple evidence with function call'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
user.isActive
}`,
description: 'Evidence with attribute access'
},
{
input: `evidence canModify(user: User, doc: Document) {
user.isActive
hasRole(user, 'admin')
}`,
description: 'Evidence with multiple conditions'
},
{
input: `evidence canDelete(user: User, doc: Document) {
owns(user, doc)
user.isActive
}`,
description: 'Evidence with ownership and status'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-basic-evidence-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.evidence.length > 0, 'Should have evidence');
});
});
test('Defeasible logic', () => {
const testCases = [
{
input: `evidence canAccess(user: User, resource: Resource) {
ALWAYS user.isActive
}`,
description: 'ALWAYS rule - strict requirement'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
WHEN hasRole(user, 'admin')
}`,
description: 'WHEN rule - defeasible condition'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}`,
description: 'WHEN/UNLESS rule - defeasible with defeater'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
REQUIRES hasClearance(user, resource.level)
}`,
description: 'REQUIRES rule - inverse defeater'
},
{
input: `evidence canAccessCritical(user: User, resource: Resource) {
ALWAYS user.isActive
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, resource.level)
}`,
description: 'Complex defeasible logic with all rule types'
},
{
input: `evidence canAccessSensitive(user: User, doc: Document) {
ALWAYS user.isActive
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, doc.level)
fusion majority {
user.isTrusted
user.hasRecentActivity
}
}`,
description: 'Defeasible logic with fusion'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-defeasible-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Pattern matching', () => {
const testCases = [
{
input: `evidence canRead(user: User, doc: Document) {
isMember(user, *group) {
canRead(group, doc)
}
}`,
description: 'Basic pattern matching with wildcard'
},
{
input: `evidence canRead(user: User, doc: Document) {
isMember(user, *group) {
canRead(group, doc)
} limit 5
}`,
description: 'Pattern matching with limit'
},
{
input: `evidence canRead(user: User, doc: Document) {
similar(doc, *similar) |similarity| {
canRead(user, similar)
} with similarity > 0.7
}`,
description: 'Pattern matching with binding and condition'
},
{
input: `evidence canRead(user: User, doc: Document) {
similar(doc, *similar) |similarity| {
canRead(user, similar)
} with similarity > 0.7 limit 5
}`,
description: 'Pattern matching with binding, condition, and limit'
},
{
input: `evidence canRead(user: User, doc: Document) {
isMember(user, *group) {
isMember(group, *parentGroup) {
canRead(parentGroup, doc)
} limit 2
} limit 3
}`,
description: 'Nested pattern matching'
},
{
input: `evidence canRead(user: User, doc: Document) {
isFriend(user, *friend) {
isMember(friend, *group) {
canRead(group, doc)
} limit 1
} limit 5
}`,
description: 'Multi-hop pattern matching'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-pattern-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Fusion', () => {
const testCases = [
{
input: `evidence canAccess(user: User, resource: Resource) {
fusion min {
hasClearance(user, resource.level)
user.isActive
}
}`,
description: 'Min fusion - all conditions must be true'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
fusion max {
hasRole(user, 'admin')
hasRole(user, 'superuser')
}
}`,
description: 'Max fusion - any condition can be true'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
fusion majority {
hasClearance(user, 'secret')
user.isTrusted
user.hasRecentActivity
}
}`,
description: 'Majority fusion - most conditions must be true'
},
{
input: `evidence canAccessCritical(user: User, resource: Resource) {
fusion min {
hasClearance(user, resource.level)
user.isActive
NOT user.isBlacklisted
}
fusion max {
hasRole(user, 'admin')
fusion majority {
hasClearance(user, 'secret')
user.isTrusted
user.lastActive within 1hr
}
}
}`,
description: 'Nested fusion with different strategies'
},
{
input: `evidence canAccess(user: User, resource: Resource) {
fusion average {
user.reputation
user.activityScore
user.verificationLevel
}
}`,
description: 'Average fusion for numeric values'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-fusion-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex evidence', () => {
const testCases = [
{
input: `evidence canRead(user: User, doc: Document) {
owns(user, doc)
isMember(user, *group) {
canRead(group, doc)
} limit 5
parentOf(user, *parent) {
canRead(parent, doc)
} limit 3
similar(doc, *similar) |similarity| {
canRead(user, similar)
} with similarity > 0.7 limit 5
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}`,
description: 'Complex evidence with all features'
},
{
input: `evidence canAccessCritical(user: User, resource: Resource) {
ALWAYS user.isActive
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, resource.level)
fusion min {
hasClearance(user, resource.level)
user.isActive
NOT user.isBlacklisted
}
fusion max {
hasRole(user, 'admin')
fusion majority {
hasClearance(user, 'secret')
user.isTrusted
user.lastActive within 1hr
}
}
}`,
description: 'Critical access with all rule types and fusion'
},
{
input: `evidence canModify(user: User, doc: Document) {
owns(user, doc)
isMember(user, *group) {
canModify(group, doc)
} limit 3
similar(doc, *similar) |similarity| {
canModify(user, similar)
similar.isEditable
} with similarity > 0.8 limit 2
fusion majority {
user.isTrusted
user.hasRecentActivity
doc.isPublic
}
}`,
description: 'Modification access with similarity and fusion'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-complex-evidence-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Evidence error handling', () => {
const testCases = [
{
input: `evidence canRead(user: User, doc: Document) {
hasRole(user, 'admin'
}`,
description: 'Missing closing parenthesis should fail'
},
{
input: `evidence canRead(user: User, doc: Document) {
WHEN hasRole(user, 'admin') UNLESS
}`,
description: 'Incomplete UNLESS condition should fail'
},
{
input: `evidence canRead(user: User, doc: Document) {
fusion min {
hasRole(user, 'admin')
}`,
description: 'Incomplete fusion should fail'
},
{
input: `evidence canRead(user: User, doc: Document) {
isMember(user, *group) {
canRead(group, doc)
} with
}`,
description: 'Incomplete with clause should fail'
},
{
input: `evidence canRead(user: User, doc: Document) {
isMember(user, *group) {
canRead(group, doc)
} limit
}`,
description: 'Incomplete limit should fail'
},
{
input: `evidence canRead(user: User, doc: Document) {
invalid syntax here
}`,
description: 'Invalid syntax should fail'
}
];
testCases.forEach(({ input, description }) => {
try {
const result = compiler.compile(input, `test-evidence-error-${Date.now()}`);
assert.ok(!result.success, `${description} should fail to parse`);
} catch {
// Expected to fail
}
});
});
});
-233
View File
@@ -1,233 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Expression Parsing', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Arithmetic operator precedence', () => {
const testCases = [
{
input: '1 + 2 * 3',
expected: 'Should evaluate as 1 + (2 * 3) = 7',
description: 'Multiplication before addition'
},
{
input: '10 - 3 * 2',
expected: 'Should evaluate as 10 - (3 * 2) = 4',
description: 'Multiplication before subtraction'
},
{
input: '8 / 2 * 4',
expected: 'Should evaluate as (8 / 2) * 4 = 16',
description: 'Left-associative division and multiplication'
},
{
input: '2 + 3 * 4 - 5',
expected: 'Should evaluate as 2 + (3 * 4) - 5 = 9',
description: 'Mixed arithmetic with correct precedence'
},
{
input: '(1 + 2) * 3',
expected: 'Should evaluate as (1 + 2) * 3 = 9',
description: 'Parentheses override precedence'
}
];
testCases.forEach(({ input, expected, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-arithmetic-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Logical operator precedence', () => {
const testCases = [
{
input: 'true && false || true',
expected: 'Should evaluate as (true && false) || true = true',
description: 'AND before OR'
},
{
input: 'false || true && false',
expected: 'Should evaluate as false || (true && false) = false',
description: 'AND before OR (alternative)'
},
{
input: 'NOT true && false',
expected: 'Should evaluate as (NOT true) && false = false',
description: 'NOT before AND'
},
{
input: 'true && NOT false',
expected: 'Should evaluate as true && (NOT false) = true',
description: 'NOT before AND (alternative)'
},
{
input: '(true || false) && true',
expected: 'Should evaluate as (true || false) && true = true',
description: 'Parentheses override logical precedence'
}
];
testCases.forEach(({ input, expected, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-logical-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Comparison operators', () => {
const testCases = [
{ input: '1 == 1', description: 'Equality comparison' },
{ input: '1 != 2', description: 'Inequality comparison' },
{ input: '5 > 3', description: 'Greater than' },
{ input: '3 < 5', description: 'Less than' },
{ input: '4 >= 4', description: 'Greater than or equal' },
{ input: '4 <= 4', description: 'Less than or equal' },
{ input: '1 == 1 && 2 > 1', description: 'Comparison with logical operators' },
{ input: '1 + 2 == 3', description: 'Arithmetic in comparison' }
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-comparison-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Temporal expressions', () => {
const testCases = [
{ input: 'user.lastActive within 1h', description: 'Temporal within expression' },
{ input: 'user.lastLogin within 24h', description: 'Temporal within with hours' },
{ input: 'user.createdAt within 7d', description: 'Temporal within with days' },
{ input: 'user.lastActivity within 1h && user.isActive', description: 'Temporal with logical operators' }
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-temporal-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Unary operators', () => {
const testCases = [
{ input: 'NOT true', description: 'NOT operator' },
{ input: '!false', description: 'Alternative NOT operator' },
{ input: 'NOT (true && false)', description: 'NOT with parenthesized expression' },
{ input: 'NOT user.isSuspended', description: 'NOT with attribute access' }
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-unary-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Attribute access', () => {
const testCases = [
{ input: 'user.role', description: 'Simple attribute access' },
{ input: 'user.profile.name', description: 'Nested attribute access' },
{ input: 'user.permissions[0]', description: 'Array access' },
{ input: 'user.role.permissions[0]', description: 'Nested attribute with array access' },
{ input: 'user.isActive && user.role == "admin"', description: 'Attribute access in logical expression' }
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-attribute-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Function calls', () => {
const testCases = [
{ input: 'hasRole(user, "admin")', description: 'Simple function call' },
{ input: 'isMember(user, group)', description: 'Function call with variables' },
{ input: 'hasPermission(user, resource, "read")', description: 'Function call with multiple arguments' },
{ input: 'hasRole(user, "admin") && isActive(user)', description: 'Multiple function calls' },
{ input: 'hasRole(user, user.role)', description: 'Function call with attribute access' }
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-function-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex expressions', () => {
const testCases = [
{
input: 'user.isActive && (hasRole(user, "admin") || hasPermission(user, resource, "read"))',
description: 'Complex logical expression with function calls'
},
{
input: 'user.balance > 100 && user.isActive && NOT user.isSuspended',
description: 'Multiple conditions with NOT'
},
{
input: 'user.lastActive within 1h && (user.role == "admin" || user.hasEmergencyAccess)',
description: 'Temporal with logical conditions'
},
{
input: 'hasRole(user, "admin") && user.isActive && NOT (user.isSuspended || user.isBlacklisted)',
description: 'Complex negation with multiple conditions'
},
{
input: 'user.score > 0.8 && user.isTrusted && user.lastActivity within 24h',
description: 'Multiple attribute conditions with temporal'
}
];
testCases.forEach(({ input, description }) => {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-complex-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Expression error handling', () => {
const testCases = [
{
input: 'user.role ==',
description: 'Incomplete comparison should fail'
},
{
input: 'user.role &&',
description: 'Incomplete logical expression should fail'
},
{
input: 'hasRole(user,)',
description: 'Function call with missing argument should fail'
},
{
input: 'user.role == "admin" &&',
description: 'Incomplete logical expression should fail'
}
];
testCases.forEach(({ input, description }) => {
try {
const dsl = `evidence test() { ${input} }`;
const result = compiler.compile(dsl, `test-error-${Date.now()}`);
assert.ok(!result.success, `${description} should fail to parse`);
} catch {
// Expected to fail
}
});
});
});
-232
View File
@@ -1,232 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Fact Declarations', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic facts', () => {
const testCases = [
{
input: `fact hasRole(user: User, role: string)`,
description: 'Simple fact with two parameters'
},
{
input: `fact isMember(user: User, group: Group)`,
description: 'Fact with custom types'
},
{
input: `fact owns(user: User, doc: Document)`,
description: 'Fact with multiple custom types'
},
{
input: `fact isActive(user: User)`,
description: 'Fact with single parameter'
},
{
input: `fact hasPermission(user: User, resource: Resource, action: string)`,
description: 'Fact with three parameters'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-basic-fact-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.facts.length > 0, 'Should have facts');
});
});
test('Fact properties', () => {
const testCases = [
{
input: `fact isMember(user: User, group: Group) transitive`,
description: 'Transitive fact'
},
{
input: `fact isFriend(user: User, friend: User) symmetrical`,
description: 'Symmetrical fact'
},
{
input: `fact isMember(user: User, group: Group) transitive symmetrical`,
description: 'Fact with multiple properties'
},
{
input: `fact isColleague(user: User, colleague: User) symmetrical`,
description: 'Symmetrical relationship fact'
},
{
input: `fact isParentOf(parent: User, child: User) transitive`,
description: 'Transitive hierarchical fact'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-fact-property-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Fact caching', () => {
const testCases = [
{
input: `fact hasRole(user: User, role: string) CACHE eager`,
description: 'Eager cached fact'
},
{
input: `fact isMember(user: User, group: Group) CACHE lazy`,
description: 'Lazy cached fact'
},
{
input: `fact isMember(user: User, group: Group) transitive CACHE eager`,
description: 'Transitive fact with eager caching'
},
{
input: `fact isFriend(user: User, friend: User) symmetrical CACHE lazy`,
description: 'Symmetrical fact with lazy caching'
},
{
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager`,
description: 'Multi-parameter fact with eager caching'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-fact-cache-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Fact limits', () => {
const testCases = [
{
input: `fact isMember(user: User, group: Group) limit 10`,
description: 'Fact with simple limit'
},
{
input: `fact isFriend(user: User, friend: User) limit 100`,
description: 'Fact with higher limit'
},
{
input: `fact isMember(user: User, group: Group) transitive limit 5`,
description: 'Transitive fact with limit'
},
{
input: `fact isFriend(user: User, friend: User) symmetrical limit 50`,
description: 'Symmetrical fact with limit'
},
{
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 3`,
description: 'Fact with properties, caching, and limit'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-fact-limit-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Parameter types', () => {
const testCases = [
{ type: 'string', description: 'String parameter' },
{ type: 'number', description: 'Number parameter' },
{ type: 'boolean', description: 'Boolean parameter' },
{ type: 'timestamp', description: 'Timestamp parameter' },
{ type: 'User', description: 'Custom type parameter' },
{ type: 'Group', description: 'Another custom type parameter' },
{ type: 'Permission[]', description: 'Array type parameter' }
];
testCases.forEach(({ type, description }) => {
const dsl = `fact test(param: ${type})`;
const result = compiler.compile(dsl, `test-param-type-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex facts', () => {
const testCases = [
{
input: `fact hasRole(user: User, role: string) CACHE eager
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
fact owns(user: User, doc: Document) CACHE eager
fact isSuspended(user: User) CACHE lazy`,
description: 'Multiple facts with different configurations'
},
{
input: `fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
fact isAdmin(user: User) CACHE eager
fact isOwner(user: User, resource: Resource) CACHE eager
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy`,
description: 'Permission-related facts'
},
{
input: `fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 20
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3`,
description: 'Relationship facts with various properties'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-complex-facts-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.facts.length > 0, 'Should have facts');
});
});
test('Fact error handling', () => {
const testCases = [
{
input: `fact hasRole(user: User, role: string`,
description: 'Missing closing parenthesis should fail'
},
{
input: `fact hasRole(user: User, )`,
description: 'Missing parameter name should fail'
},
{
input: `fact hasRole(user: User, role: )`,
description: 'Missing parameter type should fail'
},
{
input: `fact hasRole(, role: string)`,
description: 'Missing parameter name should fail'
},
{
input: `fact hasRole(user: User, role: string) CACHE`,
description: 'Incomplete cache directive should fail'
},
{
input: `fact hasRole(user: User, role: string) limit`,
description: 'Incomplete limit should fail'
},
{
input: `fact hasRole(user: User, role: string) invalid`,
description: 'Invalid property should fail'
}
];
testCases.forEach(({ input, description }) => {
try {
const result = compiler.compile(input, `test-fact-error-${Date.now()}`);
assert.ok(!result.success, `${description} should fail to parse`);
} catch {
// Expected to fail
}
});
});
});
-534
View File
@@ -1,534 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Integration Tests', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Complete authorization system', () => {
const completeSystem = `
// Type definitions with complex behaviors
definition User {
role: string
isActive: boolean
lastActive: timestamp BEHAVES {
decaying down hourly
} CACHE lazy
isSuspended: boolean
balance: number BEHAVES {
decaying down hourly
} CACHE eager
score: number BEHAVES {
blurring adaptive confidence_95
} CACHE lazy
session: string BEHAVES {
ttl 24h
} CACHE eager
clearance: string BEHAVES {
blurring fixed
} CACHE eager
reputation: number BEHAVES {
decaying up daily
} CACHE lazy
}
definition Group {
name: string
permissions: Permission[]
level: string
isPublic: boolean CACHE eager
created: timestamp BEHAVES {
decaying stable monthly
} CACHE lazy
}
definition Document {
level: string
owner: User
tags: string[]
content: string BEHAVES {
blurring fixed
} CACHE lazy
accessCount: number BEHAVES {
decaying up daily
} CACHE eager
expiresAt: timestamp BEHAVES {
ttl 30d
} CACHE eager
isPublic: boolean CACHE eager
}
definition Resource {
level: string
owner: User
permissions: Permission[]
isPublic: boolean CACHE eager
accessCount: number BEHAVES {
decaying up daily
} CACHE eager
}
// Facts with various properties and caching
fact hasRole(user: User, role: string) CACHE eager
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
fact owns(user: User, doc: Document) CACHE eager
fact isSuspended(user: User) CACHE lazy
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
fact isAdmin(user: User) CACHE eager
fact isOwner(user: User, resource: Resource) CACHE eager
fact hasAccess(user: User, resource: Resource, level: string) CACHE lazy
fact isColleague(user: User, colleague: User) symmetrical CACHE lazy limit 50
fact isParentOf(parent: User, child: User) transitive CACHE eager limit 3
// Evidence rules with complex logic
evidence canRead(user: User, doc: Document) {
owns(user, doc)
isMember(user, *group) {
canRead(group, doc)
} limit 5
parentOf(user, *parent) {
canRead(parent, doc)
} limit 3
similar(doc, *similar) |similarity| {
canRead(user, similar)
} with similarity > 0.7 limit 5
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}
evidence canWrite(user: User, doc: Document) {
owns(user, doc)
isMember(user, *group) {
canWrite(group, doc)
} limit 3
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES user.isActive
}
evidence canDelete(user: User, doc: Document) {
owns(user, doc)
ALWAYS user.isActive
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES user.isActive
}
evidence canAccessCritical(user: User, resource: Resource) {
fusion min {
hasClearance(user, resource.level)
user.isActive
NOT user.isBlacklisted
}
fusion max {
hasRole(user, 'admin')
fusion majority {
hasClearance(user, 'secret')
user.isTrusted
user.lastActive within 1hr
}
}
}
evidence canAccessSensitive(user: User, doc: Document) {
ALWAYS user.isActive
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, doc.level)
fusion majority {
user.isTrusted
user.hasRecentActivity
}
}
// Measures for computed values
measure userRole(user: User) {
user.role
} PROVIDES string
measure userPermissions(user: User) {
fusion max {
user.role.permissions
user.group.permissions
}
} PROVIDES Permission[]
measure effectiveClearance(user: User) {
fusion majority {
user.clearance
user.role.clearance
user.group.clearance
}
} PROVIDES string
measure userTrustScore(user: User) {
fusion average {
user.reputation
user.activityScore
user.verificationLevel
}
} PROVIDES number
measure userBalance(user: User) {
user.balance
} PROVIDES number
measure userScore(user: User) {
user.score
} PROVIDES number
`;
const result = compiler.compile(completeSystem, 'test-complete-system');
assert.ok(result.success, 'Complete authorization system should compile successfully');
assert.ok(result.program.definitions.length >= 4, 'Should have multiple definitions');
assert.ok(result.program.facts.length >= 10, 'Should have multiple facts');
assert.ok(result.program.evidence.length >= 5, 'Should have multiple evidence rules');
assert.ok(result.program.measures.length >= 6, 'Should have multiple measures');
});
test('Multi-domain system', () => {
const multiDomain = `
// Authentication domain
definition User {
role: string
isActive: boolean
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
session: string BEHAVES { ttl 24h } CACHE eager
}
fact hasRole(user: User, role: string) CACHE eager
fact isActive(user: User) CACHE eager
evidence canAuthenticate(user: User) {
user.isActive
user.session within 24h
}
// Authorization domain
definition Resource {
level: string
owner: User
permissions: Permission[]
}
fact owns(user: User, resource: Resource) CACHE eager
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
evidence canAccess(user: User, resource: Resource) {
owns(user, resource)
hasPermission(user, resource, 'read')
}
// Finance domain
definition Account {
balance: number BEHAVES { decaying down hourly } CACHE eager
owner: User
isActive: boolean CACHE eager
}
fact hasAccount(user: User, account: Account) CACHE eager
fact hasBalance(user: User, amount: number) CACHE eager
evidence canWithdraw(user: User, amount: number) {
hasBalance(user, amount)
user.isActive
}
// Social domain
definition Group {
name: string
members: User[]
isPublic: boolean CACHE eager
}
fact isMember(user: User, group: Group) transitive CACHE lazy limit 10
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
evidence canAccessGroup(user: User, group: Group) {
isMember(user, group)
group.isPublic
}
`;
const result = compiler.compile(multiDomain, 'test-multi-domain');
assert.ok(result.success, 'Multi-domain system should compile successfully');
assert.ok(result.program.definitions.length >= 4, 'Should have multiple domain definitions');
assert.ok(result.program.facts.length >= 8, 'Should have multiple domain facts');
assert.ok(result.program.evidence.length >= 4, 'Should have multiple domain evidence rules');
});
test('Hierarchical access', () => {
const hierarchicalSystem = `
definition User {
role: string
level: string
isActive: boolean
clearance: string
}
definition Organization {
name: string
level: string
parent: Organization
}
fact isMember(user: User, org: Organization) transitive CACHE lazy limit 5
fact isParentOf(parent: Organization, child: Organization) transitive CACHE eager limit 3
fact hasRole(user: User, role: string) CACHE eager
fact hasClearance(user: User, level: string) CACHE eager
evidence canAccessOrg(user: User, org: Organization) {
isMember(user, org)
isParentOf(org, *parentOrg) {
canAccessOrg(user, parentOrg)
} limit 3
WHEN hasRole(user, 'admin') UNLESS user.isSuspended
}
evidence canAccessResource(user: User, resource: Resource) {
isMember(user, *org) {
canAccessResource(org, resource)
} limit 5
parentOf(user, *parent) {
canAccessResource(parent, resource)
} limit 2
}
`;
const result = compiler.compile(hierarchicalSystem, 'test-hierarchical');
assert.ok(result.success, 'Hierarchical access system should compile successfully');
});
test('Similarity-based access', () => {
const similaritySystem = `
definition User {
profile: string
interests: string[]
isActive: boolean
}
definition Document {
content: string
tags: string[]
isPublic: boolean
owner: User
}
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 100
fact hasInterest(user: User, interest: string) CACHE lazy
fact hasTag(doc: Document, tag: string) CACHE lazy
evidence canRead(user: User, doc: Document) {
owns(user, doc)
similar(doc, *similar) |similarity| {
canRead(user, similar)
similar.isPublic
} with similarity > 0.7 limit 10
isFriend(user, *friend) {
canRead(friend, doc)
} limit 5
fusion majority {
user.interests
doc.tags
}
}
evidence canRecommend(user: User, doc: Document) {
similar(user, *similarUser) |similarity| {
canRead(similarUser, doc)
} with similarity > 0.8 limit 20
fusion average {
user.profile
doc.content
}
}
`;
const result = compiler.compile(similaritySystem, 'test-similarity');
assert.ok(result.success, 'Similarity-based access system should compile successfully');
});
test('Temporal access', () => {
const temporalSystem = `
definition User {
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
session: string BEHAVES { ttl 24h } CACHE eager
isActive: boolean
}
definition Event {
startTime: timestamp
endTime: timestamp
isPublic: boolean
}
fact hasAccess(user: User, event: Event) CACHE lazy
fact isParticipant(user: User, event: Event) CACHE eager
evidence canAccessEvent(user: User, event: Event) {
user.lastActive within 1h
isParticipant(user, event)
WHEN event.isPublic UNLESS user.isSuspended
fusion min {
user.session within 24h
user.isActive
}
}
evidence canAccessHistorical(user: User, event: Event) {
user.lastActive within 24h
fusion majority {
user.isActive
user.session within 24h
event.isPublic
}
}
`;
const result = compiler.compile(temporalSystem, 'test-temporal');
assert.ok(result.success, 'Temporal access system should compile successfully');
});
test('Complex behaviors', () => {
const behaviorSystem = `
definition User {
balance: number BEHAVES { decaying down hourly } CACHE eager
score: number BEHAVES { blurring adaptive confidence_95 } CACHE lazy
session: string BEHAVES { ttl 24h } CACHE eager
reputation: number BEHAVES { decaying up daily } CACHE lazy
clearance: string BEHAVES { blurring fixed } CACHE eager
lastActive: timestamp BEHAVES { decaying down hourly } CACHE lazy
}
definition Document {
content: string BEHAVES { blurring fixed } CACHE lazy
accessCount: number BEHAVES { decaying up daily } CACHE eager
expiresAt: timestamp BEHAVES { ttl 30d } CACHE eager
isPublic: boolean CACHE eager
}
fact hasBalance(user: User, amount: number) CACHE eager
fact hasScore(user: User, score: number) CACHE lazy
fact hasReputation(user: User, reputation: number) CACHE lazy
evidence canAccessDocument(user: User, doc: Document) {
user.balance > 0
user.score > 0.5
user.reputation > 0.3
doc.accessCount < 1000
fusion majority {
user.isActive
user.lastActive within 1h
doc.isPublic
}
}
measure userEffectiveScore(user: User) {
fusion average {
user.score
user.reputation
user.balance
}
} PROVIDES number
measure documentPopularity(doc: Document) {
doc.accessCount
} PROVIDES number
`;
const result = compiler.compile(behaviorSystem, 'test-behaviors');
assert.ok(result.success, 'Complex behaviors system should compile successfully');
});
test('Performance scenarios', () => {
const performanceSystem = `
definition User {
role: string
isActive: boolean
permissions: Permission[] CACHE eager
}
definition Resource {
level: string
owner: User
permissions: Permission[] CACHE eager
}
// High-frequency facts with limits
fact isMember(user: User, group: Group) transitive CACHE lazy limit 5
fact isFriend(user: User, friend: User) symmetrical CACHE eager limit 50
fact hasPermission(user: User, resource: Resource, action: string) CACHE eager
fact owns(user: User, resource: Resource) CACHE eager
// Optimized evidence rules
evidence canAccess(user: User, resource: Resource) {
owns(user, resource)
isMember(user, *group) {
canAccess(group, resource)
} limit 3
WHEN hasPermission(user, resource, 'read')
}
evidence canModify(user: User, resource: Resource) {
owns(user, resource)
isMember(user, *group) {
canModify(group, resource)
} limit 2
WHEN hasPermission(user, resource, 'write')
}
// Efficient measures
measure userEffectivePermissions(user: User) {
user.permissions
} PROVIDES Permission[]
measure resourceAccessLevel(resource: Resource) {
resource.level
} PROVIDES string
`;
const result = compiler.compile(performanceSystem, 'test-performance');
assert.ok(result.success, 'Performance scenarios should compile successfully');
});
});
-306
View File
@@ -1,306 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
describe('Measure Definitions', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic measures', () => {
const testCases = [
{
input: `measure userRole(user: User) {
user.role
} PROVIDES string`,
description: 'Simple measure with attribute access'
},
{
input: `measure userBalance(user: User) {
user.balance
} PROVIDES number`,
description: 'Measure accessing numeric attribute'
},
{
input: `measure isUserActive(user: User) {
user.isActive
} PROVIDES boolean`,
description: 'Measure accessing boolean attribute'
},
{
input: `measure userPermissions(user: User) {
user.permissions
} PROVIDES Permission[]`,
description: 'Measure accessing array attribute'
},
{
input: `measure userScore(user: User) {
user.score
} PROVIDES number`,
description: 'Measure with behavior-inherited attribute'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-basic-measure-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.measures.length > 0, 'Should have measures');
});
});
test('Measure return types', () => {
const testCases = [
{ type: 'string', description: 'String return type' },
{ type: 'number', description: 'Number return type' },
{ type: 'boolean', description: 'Boolean return type' },
{ type: 'timestamp', description: 'Timestamp return type' },
{ type: 'Permission[]', description: 'Array return type' },
{ type: 'User', description: 'Custom type return' },
{ type: 'Group[]', description: 'Custom array return type' }
];
testCases.forEach(({ type, description }) => {
const dsl = `measure test() { true } PROVIDES ${type}`;
const result = compiler.compile(dsl, `test-measure-return-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Measure aggregation', () => {
const testCases = [
{
input: `measure userPermissions(user: User) {
aggregate {
user.role.permissions
user.group.permissions
} USING majority
} PROVIDES Permission[]`,
description: 'Aggregation with majority strategy'
},
{
input: `measure userClearance(user: User) {
aggregate {
user.clearance
user.role.clearance
user.group.clearance
} USING max
} PROVIDES string`,
description: 'Aggregation with max strategy'
},
{
input: `measure userScore(user: User) {
aggregate {
user.reputation
user.activityScore
user.verificationLevel
} USING average
} PROVIDES number`,
description: 'Aggregation with average strategy'
},
{
input: `measure userTrust(user: User) {
aggregate {
user.reputation
user.activityScore
user.verificationLevel
user.socialProof
} USING min
} PROVIDES number`,
description: 'Aggregation with min strategy'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-measure-aggregation-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Measure fusion', () => {
const testCases = [
{
input: `measure effectiveClearance(user: User) {
fusion max {
user.clearance
user.role.clearance
user.group.clearance
}
} PROVIDES string`,
description: 'Fusion with max strategy'
},
{
input: `measure userPermissions(user: User) {
fusion min {
user.role.permissions
user.group.permissions
}
} PROVIDES Permission[]`,
description: 'Fusion with min strategy'
},
{
input: `measure userScore(user: User) {
fusion majority {
user.reputation
user.activityScore
user.verificationLevel
}
} PROVIDES number`,
description: 'Fusion with majority strategy'
},
{
input: `measure userTrust(user: User) {
fusion average {
user.reputation
user.activityScore
user.verificationLevel
user.socialProof
}
} PROVIDES number`,
description: 'Fusion with average strategy'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-measure-fusion-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex measures', () => {
const testCases = [
{
input: `measure userEffectivePermissions(user: User) {
aggregate {
user.role.permissions
user.group.permissions
user.directPermissions
} USING majority
} PROVIDES Permission[]`,
description: 'Complex aggregation with multiple sources'
},
{
input: `measure userTrustScore(user: User) {
fusion average {
user.reputation
user.activityScore
user.verificationLevel
user.socialProof
user.peerRatings
}
} PROVIDES number`,
description: 'Complex fusion with multiple metrics'
},
{
input: `measure userAccessLevel(user: User) {
fusion max {
user.clearance
user.role.clearance
user.group.clearance
user.temporaryClearance
}
} PROVIDES string`,
description: 'Complex clearance calculation'
},
{
input: `measure userSimilarity(user1: User, user2: User) {
similar(user1, user2) |similarity| {
similarity
} with similarity > 0.5
} PROVIDES number`,
description: 'Similarity measure with pattern matching'
},
{
input: `measure userEffectiveRole(user: User) {
fusion majority {
user.role
user.temporaryRole
user.actingRole
}
} PROVIDES string`,
description: 'Role determination with multiple sources'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(input, `test-complex-measure-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Measure error handling', () => {
const testCases = [
{
input: `measure userRole(user: User) {
user.role
}`,
description: 'Missing PROVIDES clause should fail',
expectSuccess: false
},
{
input: `measure userRole(user: User) {
user.role
} PROVIDES`,
description: 'Incomplete PROVIDES clause should fail',
expectSuccess: false
},
{
input: `measure userRole(user: User) {
user.role
} PROVIDES string`,
description: 'Valid measure should succeed',
expectSuccess: true
},
{
input: `measure userPermissions(user: User) {
aggregate {
user.role.permissions
user.group.permissions
} USING
} PROVIDES Permission[]`,
description: 'Incomplete USING clause should fail',
expectSuccess: false
},
{
input: `measure userScore(user: User) {
fusion {
user.reputation
user.activityScore
}
} PROVIDES number`,
description: 'Missing fusion strategy should fail',
expectSuccess: false
},
{
input: `measure userRole(user: User) {
invalid syntax here
} PROVIDES string`,
description: 'Invalid syntax should fail',
expectSuccess: false
}
];
testCases.forEach(({ input, description, expectSuccess }) => {
try {
const result = compiler.compile(input, `test-measure-error-${Date.now()}`);
if (expectSuccess) {
assert.ok(result.success, `${description} should parse successfully`);
} else {
assert.ok(!result.success, `${description} should fail to parse`);
}
} catch {
if (!expectSuccess) {
// Expected to fail
}
}
});
});
});
-101
View File
@@ -1,101 +0,0 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { PeggyDSLParser } from '../../src/ast/parser/PeggyDSLParser.js';
describe('Peggy DSL Parser', () => {
const parser = new PeggyDSLParser();
test('Basic parsing', () => {
const dsl = `
definition User {
role: string
isActive: boolean
}
fact hasRole(user: User, role: string)
evidence canRead(user: User, doc: Document) {
hasRole(user, 'admin')
}
`;
const program = parser.parse(dsl);
assert.ok(program !== null, 'Program should be created');
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
assert.ok(program.facts.length === 1, 'Should have 1 fact');
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
});
test('Complex DSL parsing', () => {
const dsl = `
definition User {
role: string
isActive: boolean
clearance: string BEHAVES {
blurring adaptive confidence_95
} CACHE eager
}
fact hasRole(user: User, role: string) CACHE eager
fact isMember(user: User, group: Group) transitive CACHE lazy
evidence canRead(user: User, doc: Document) {
hasRole(user, 'admin')
isMember(user, *group) {
canRead(group, doc)
} limit 5
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}
`;
const program = parser.parse(dsl);
assert.ok(program !== null, 'Program should be created');
assert.ok(program.definitions.length === 1, 'Should have 1 definition');
assert.ok(program.facts.length === 2, 'Should have 2 facts');
assert.ok(program.evidence.length === 1, 'Should have 1 evidence');
});
test('Error handling', () => {
const invalidDSL = `
definition User {
role: string
// Missing closing brace
fact hasRole(user: User, role: string)
// Missing semicolon
`;
assert.throws(
() => parser.parse(invalidDSL),
/Parsing failed/,
'Should have parsing error message'
);
});
test('Validation', () => {
const validDSL = `
definition User {
role: string
isActive: boolean
}
fact hasRole(user: User, role: string)
`;
const invalidDSL = `
definition User {
role: string
// Missing closing brace
`;
const validResult = parser.validate(validDSL);
assert.ok(validResult.success, 'Valid DSL should pass validation');
assert.ok(validResult.program !== null, 'Valid DSL should return program');
const invalidResult = parser.validate(invalidDSL);
assert.ok(!invalidResult.success, 'Invalid DSL should fail validation');
assert.ok(invalidResult.errors.length > 0, 'Should have validation errors');
});
});
-249
View File
@@ -1,249 +0,0 @@
# Evidence DSL Test Suite
## Overview
This comprehensive test suite follows a **structural linguistic approach** to validate the Evidence DSL (Domain Specific Language) for authorization policies. The tests are organized incrementally from basic language primitives to complex integration scenarios.
## Test Structure
### 1. Structural Linguistic Tests (`StructuralLinguisticTests.js`)
**Level: Comprehensive**
- **Lexical Primitives**: Identifiers, literals, keywords, whitespace
- **Basic Expressions**: Arithmetic, logical, comparison, temporal
- **Type System**: Definitions, fields, behaviors, caching
- **Fact System**: Declarations, properties, caching, limits
- **Evidence System**: Rules, defeasible logic, pattern matching
- **Measure System**: Aggregation, fusion, return types
- **Complex Integration**: Multi-feature combinations
### 2. Expression Tests (`ExpressionTests.js`)
**Level: Focused**
- Arithmetic operator precedence
- Logical operator precedence
- Comparison operators
- Temporal expressions
- Unary operators
- Attribute access
- Function calls
- Complex expressions
- Error handling
### 3. Definition Tests (`DefinitionTests.js`)
**Level: Focused**
- Basic type definitions
- Field types (string, number, boolean, timestamp, custom)
- Array types
- Behaviors (decay, blur, TTL)
- Caching (eager, lazy)
- Complex definitions
- Error handling
### 4. Fact Tests (`FactTests.js`)
**Level: Focused**
- Basic fact declarations
- Fact properties (transitive, symmetrical)
- Fact caching
- Fact limits
- Parameter types
- Complex facts
- Error handling
### 5. Evidence Tests (`EvidenceTests.js`)
**Level: Focused**
- Basic evidence rules
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
- Pattern matching with wildcards
- Fusion strategies (min, max, majority, average)
- Complex evidence composition
- Error handling
### 6. Measure Tests (`MeasureTests.js`)
**Level: Focused**
- Basic measure definitions
- Return types
- Aggregation with different strategies
- Fusion with different strategies
- Complex measures
- Error handling
### 7. Integration Tests (`IntegrationTests.js`)
**Level: Integration**
- Complete authorization systems
- Multi-domain systems
- Hierarchical access patterns
- Similarity-based access
- Temporal access patterns
- Complex behaviors
- Performance scenarios
## Test Runner (`TestRunner.js`)
The test runner orchestrates all test suites and provides:
- **Comprehensive Testing**: Run all test suites
- **Selective Testing**: Run specific test suites
- **Level-based Testing**: Run tests by complexity level
- **Detailed Reporting**: Summary and detailed results
- **Coverage Analysis**: Language feature coverage
## Usage
### Run All Tests
```javascript
import { runAllTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
const results = runAllTests(arbiter);
console.log(`Tests: ${results.passed}/${results.total} passed`);
```
### Run Specific Test Suites
```javascript
import { runSpecificTests } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
const results = runSpecificTests(arbiter, [
'Expression Tests',
'Definition Tests'
]);
```
### Run Tests by Level
```javascript
import { runTestsByLevel } from '../../../../../lib/src/ast/tests/tests/TestRunner.js';
// Run only focused tests
const results = runTestsByLevel(arbiter, 'focused');
// Run only integration tests
const results = runTestsByLevel(arbiter, 'integration');
```
## Language Feature Coverage
### ✅ Lexical Primitives
- Identifiers (simple, with underscores, with numbers)
- Literals (string, number, boolean, duration)
- Keywords (reserved words)
- Whitespace and comments
### ✅ Expression System
- Arithmetic operators (+, -, *, /) with precedence
- Logical operators (&&, ||, NOT) with precedence
- Comparison operators (==, !=, >, <, >=, <=)
- Temporal expressions (within)
- Unary operators (NOT, !)
- Attribute access (object.attribute)
- Function calls (predicate(args))
### ✅ Type System
- Type definitions with fields
- Field types (string, number, boolean, timestamp, custom)
- Array types (Type[])
- Behaviors (decay, blur, TTL)
- Caching directives (eager, lazy)
### ✅ Fact System
- Fact declarations with parameters
- Fact properties (transitive, symmetrical)
- Caching directives
- Limits for performance
- Parameter types
### ✅ Evidence System
- Basic evidence rules
- Defeasible logic (ALWAYS, WHEN/UNLESS, REQUIRES)
- Pattern matching with wildcards (*)
- Binding clauses (|variable|)
- With clauses (with condition)
- Limits for pattern matching
- Fusion strategies (min, max, majority, average)
### ✅ Measure System
- Measure definitions
- Return type specifications (PROVIDES)
- Aggregation with strategies (USING)
- Fusion with strategies
- Complex value computation
### ✅ Integration Features
- Multi-domain systems
- Hierarchical access patterns
- Similarity-based access
- Temporal access patterns
- Complex behavior combinations
- Performance optimization scenarios
## Test Philosophy
### Structural Linguistic Approach
The tests follow a structural linguistic methodology:
1. **Phonological Level**: Basic lexical elements (identifiers, literals)
2. **Morphological Level**: Word formation (operators, keywords)
3. **Syntactic Level**: Grammar rules (expressions, statements)
4. **Semantic Level**: Meaning (types, behaviors, logic)
5. **Pragmatic Level**: Usage (integration, real-world scenarios)
### Incremental Complexity
Tests progress from simple to complex:
- **Level 1**: Lexical primitives
- **Level 2**: Basic expressions
- **Level 3**: Type system
- **Level 4**: Fact system
- **Level 5**: Evidence system
- **Level 6**: Measure system
- **Level 7**: Complex integration
### Comprehensive Coverage
Each language feature is tested for:
- **Valid cases**: Correct syntax and semantics
- **Invalid cases**: Error handling and recovery
- **Edge cases**: Boundary conditions
- **Integration**: Multi-feature combinations
## Running Tests
### Prerequisites
- Node.js environment
- Arbiter instance for testing
- All dependencies installed
### Basic Usage
```bash
# Run all tests
npm test
# Run specific test file
node src/ast/tests/StructuralLinguisticTests.js
# Run with specific arbiter
node -e "
import { runAllTests } from '../../../../../lib/src/ast/tests/src/ast/tests/TestRunner.js';
const results = runAllTests(arbiter);
console.log(results);
"
```
### Test Output
The test runner provides:
- **Progress indicators**: Real-time test execution
- **Detailed results**: Pass/fail status for each test
- **Error reporting**: Specific error messages for failures
- **Performance metrics**: Execution time for each suite
- **Coverage analysis**: Language feature coverage
## Contributing
When adding new tests:
1. Follow the structural linguistic approach
2. Test both valid and invalid cases
3. Include error handling tests
4. Document test purpose and expected behavior
5. Maintain incremental complexity
6. Update coverage documentation
## Test Maintenance
- **Regular Updates**: Keep tests current with language changes
- **Performance Monitoring**: Track test execution time
- **Coverage Analysis**: Ensure comprehensive feature coverage
- **Error Handling**: Validate error messages and recovery
- **Integration Testing**: Test real-world scenarios
+1 -1
View File
@@ -11,7 +11,7 @@ import {
evaluateBuiltIn, evaluateBuiltIn,
getFunctionSignature, getFunctionSignature,
BUILT_IN_FUNCTIONS BUILT_IN_FUNCTIONS
} from '../../src/ast/interpreter/BuiltInFunctions.js'; } from '@arbiter/evidence-dsl/interpreter/BuiltInFunctions';
describe('Built-in Functions Registry', () => { describe('Built-in Functions Registry', () => {
it('should identify built-in functions', () => { it('should identify built-in functions', () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from 'node:test'; import { describe, test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { validateDslText } from '../../src/ast/validation/DSLValidation.js'; import { validateDslText } from '@arbiter/evidence-dsl/validation/DSLValidation';
describe('DSL injectable predicates (* prefix)', () => { describe('DSL injectable predicates (* prefix)', () => {
test('allows injectable predicates with * prefix in evidence bodies', () => { test('allows injectable predicates with * prefix in evidence bodies', () => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from 'node:test'; import { describe, test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { validateDslText } from '../../src/ast/validation/DSLValidation.js'; import { validateDslText } from '@arbiter/evidence-dsl/validation/DSLValidation';
describe('DSL type guards', () => { describe('DSL type guards', () => {
test('infix is guard narrows for attribute access', () => { test('infix is guard narrows for attribute access', () => {
+3 -3
View File
@@ -23,7 +23,7 @@
import { describe, it } from 'node:test'; import { describe, it } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { rigor } from '@rigor/core'; import { rigor } from '@rigor/core';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js'; import { DSLCompiler } from '@arbiter/evidence-dsl/DSLCompiler';
// Built-in types from lib/src/ast/validation/DSLPrelude.js are reserved (User, Account, // Built-in types from lib/src/ast/validation/DSLPrelude.js are reserved (User, Account,
// Device, AuthSession). Use non-reserved names so the validator accepts the DSL. // Device, AuthSession). Use non-reserved names so the validator accepts the DSL.
@@ -215,8 +215,8 @@ describe('DSLCompiler → engine rule mapping (rigor)', () => {
// multi_hop shape uses a plain variable binding instead. We bypass the // multi_hop shape uses a plain variable binding instead. We bypass the
// validator by directly exercising the parser→generator path: parse // validator by directly exercising the parser→generator path: parse
// only, then run the generator against the parsed AST. // only, then run the generator against the parsed AST.
const { parse } = await import('../../src/ast/parser/DSLParser.js'); const { parse } = await import('@arbiter/evidence-dsl/parser/DSLParser');
const { RuleGenerator } = await import('../../src/ast/generator/RuleGenerator.js'); const { RuleGenerator } = await import('@arbiter/evidence-dsl/generator/RuleGenerator');
const dsl = `${DEFINITIONS}${FACTS}\nevidence canReach(a: Person, b: Person) { friend_of(a, f) { friend_of(f, b) } limit 5 }\n`; const dsl = `${DEFINITIONS}${FACTS}\nevidence canReach(a: Person, b: Person) { friend_of(a, f) { friend_of(f, b) } limit 5 }\n`;
const program = parse(dsl); const program = parse(dsl);
const generator = new RuleGenerator(arbiter); const generator = new RuleGenerator(arbiter);
+1 -1
View File
@@ -17,7 +17,7 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { rigor } from '@rigor/core'; import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js'; import { Arbiter } from '../../src/index.js';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js'; import { DSLCompiler } from '@arbiter/evidence-dsl/DSLCompiler';
const EPS = 1e-9; const EPS = 1e-9;
const POS = [0, 0.25, 0.5, 0.75, 1]; const POS = [0, 0.25, 0.5, 0.75, 1];
@@ -20,7 +20,7 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { rigor } from '@rigor/core'; import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js'; import { Arbiter } from '../../src/index.js';
import { DSLCompiler } from '../../src/ast/DSLCompiler.js'; import { DSLCompiler } from '@arbiter/evidence-dsl/DSLCompiler';
const EPS = 1e-9; const EPS = 1e-9;
const POSSIBILITIES = [0, 0.25, 0.5, 0.75, 1]; const POSSIBILITIES = [0, 0.25, 0.5, 0.75, 1];
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from 'node:test'; import { describe, test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { validateDslText } from '../../src/ast/validation/DSLValidation.js'; import { validateDslText } from '@arbiter/evidence-dsl/validation/DSLValidation';
describe.skip('DSL injectable predicates (* prefix)', () => { describe.skip('DSL injectable predicates (* prefix)', () => {
test('allows injectable predicates with * prefix in evidence bodies', () => { test('allows injectable predicates with * prefix in evidence bodies', () => {
+2 -2
View File
@@ -1,5 +1,5 @@
import { parse } from '../../src/ast/parser/GeneratedParser.js'; import { parse } from '@arbiter/evidence-dsl/parser/GeneratedParser';
import { RuleGenerator } from '../../src/ast/generator/RuleGenerator.js'; import { RuleGenerator } from '@arbiter/evidence-dsl/generator/RuleGenerator';
import { Arbiter } from '../../src/core/Arbiter.js'; import { Arbiter } from '../../src/core/Arbiter.js';
const dsl = [ const dsl = [