evidence-dsl: extract Evidence DSL v2 compiler from @arbiter/core
The Evidence DSL (ADR-000) is a thin declarative layer that compiles to engine rule types. It has zero runtime coupling to the core engine (DSLCompiler takes an arbiter as a duck-typed argument; the only shared code was the ip-utils helpers, now local). Extracting it into its own package keeps the core artifact free of the DSL surface. - @arbiter/evidence-dsl depends on @arbiter/core (config formats are the compilation target) - deep-path exports for the compiler, parser, generator, validation, and built-in functions (the surface the core's DSL tests consume) - tests moved alongside; generate-parser script + peggy devDep local - CI: test on push, publish on v* tags
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import { parse } from './parser/DSLParser.js';
|
||||
import { RuleGenerator } from './generator/RuleGenerator.js';
|
||||
import { validateDslText } from './validation/DSLValidation.js';
|
||||
|
||||
/**
|
||||
* DSL Compiler - Main integration layer
|
||||
* Compiles DSL text into rule configurations for the zanzibar-graph system
|
||||
*/
|
||||
export class DSLCompiler {
|
||||
constructor(arbiter) {
|
||||
this.arbiter = arbiter;
|
||||
this.parser = parse;
|
||||
this.generator = new RuleGenerator(arbiter);
|
||||
this.compiledPrograms = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile DSL text into rule configurations
|
||||
* @param {string} dslText - DSL text to compile
|
||||
* @param {string} programName - Optional name for the program
|
||||
* @returns {Object} Compilation result
|
||||
*/
|
||||
compile(dslText, programName = 'default') {
|
||||
try {
|
||||
const validation = validateDslText(dslText);
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: null,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
const program = validation.program;
|
||||
|
||||
// Convert plain AST to ProgramNode structure
|
||||
const programNode = {
|
||||
definitions: program.body.filter(s => s.type === 'Definition'),
|
||||
facts: program.body.filter(s => s.type === 'Fact'),
|
||||
evidence: program.body.filter(s => s.type === 'Evidence'),
|
||||
measures: program.body.filter(s => s.type === 'Measure'),
|
||||
validate: () => ({ isValid: true, errors: [], warnings: [] })
|
||||
};
|
||||
|
||||
// Generate rules from AST
|
||||
const generationResult = this.generator.generateRules(programNode);
|
||||
|
||||
if (!generationResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: generationResult.errors,
|
||||
warnings: [],
|
||||
program: programNode,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
// Store compiled program
|
||||
this.compiledPrograms.set(programName, {
|
||||
program: programNode,
|
||||
generatedRules: this.generator.getGeneratedRules(),
|
||||
dependencyIndex: this.generator.getDependencyIndex(),
|
||||
compiledAt: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: programNode,
|
||||
generatedRules: this.generator.getGeneratedRules(),
|
||||
dependencyIndex: this.generator.getDependencyIndex(),
|
||||
generatedCount: generationResult.generatedCount
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [`Compilation error: ${error.message}`],
|
||||
warnings: [],
|
||||
program: null,
|
||||
generatedRules: new Map()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile multiple DSL programs
|
||||
* @param {Object} programs - Map of program names to DSL text
|
||||
* @returns {Object} Compilation result for all programs
|
||||
*/
|
||||
compileMultiple(programs) {
|
||||
const results = {};
|
||||
let overallSuccess = true;
|
||||
const allErrors = [];
|
||||
const allWarnings = [];
|
||||
|
||||
for (const [name, dslText] of Object.entries(programs)) {
|
||||
const result = this.compile(dslText, name);
|
||||
results[name] = result;
|
||||
|
||||
if (!result.success) {
|
||||
overallSuccess = false;
|
||||
}
|
||||
|
||||
allErrors.push(...result.errors.map(err => `${name}: ${err}`));
|
||||
allWarnings.push(...result.warnings.map(warn => `${name}: ${warn}`));
|
||||
}
|
||||
|
||||
return {
|
||||
success: overallSuccess,
|
||||
errors: allErrors,
|
||||
warnings: allWarnings,
|
||||
results: results
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compiled program by name
|
||||
* @param {string} programName - Name of the program
|
||||
* @returns {Object|null} Compiled program or null
|
||||
*/
|
||||
getCompiledProgram(programName) {
|
||||
return this.compiledPrograms.get(programName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all compiled programs
|
||||
* @returns {Map} Map of all compiled programs
|
||||
*/
|
||||
getAllCompiledPrograms() {
|
||||
return this.compiledPrograms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove compiled program
|
||||
* @param {string} programName - Name of the program to remove
|
||||
* @returns {boolean} True if removed successfully
|
||||
*/
|
||||
removeCompiledProgram(programName) {
|
||||
return this.compiledPrograms.delete(programName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all compiled programs
|
||||
*/
|
||||
clearCompiledPrograms() {
|
||||
this.compiledPrograms.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compilation statistics
|
||||
* @returns {Object} Compilation statistics
|
||||
*/
|
||||
getCompilationStats() {
|
||||
const stats = {
|
||||
totalPrograms: this.compiledPrograms.size,
|
||||
totalRules: 0,
|
||||
programs: {}
|
||||
};
|
||||
|
||||
this.compiledPrograms.forEach((program, name) => {
|
||||
const programStats = {
|
||||
name: name,
|
||||
compiledAt: program.compiledAt,
|
||||
ruleCount: program.generatedRules.size,
|
||||
definitions: program.program.definitions.length,
|
||||
facts: program.program.facts.length,
|
||||
evidence: program.program.evidence.length,
|
||||
measures: program.program.measures.length
|
||||
};
|
||||
|
||||
stats.programs[name] = programStats;
|
||||
stats.totalRules += program.generatedRules.size;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate DSL text without compiling
|
||||
* @param {string} dslText - DSL text to validate
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
validate(dslText) {
|
||||
const validation = validateDslText(dslText);
|
||||
return {
|
||||
success: validation.success,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
program: validation.program
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parser errors from last parse
|
||||
* @returns {string[]} Array of parser errors
|
||||
*/
|
||||
getParserErrors() {
|
||||
return this.parser.getErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get generator errors from last generation
|
||||
* @returns {string[]} Array of generator errors
|
||||
*/
|
||||
getGeneratorErrors() {
|
||||
return this.generator.getErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a relation is configured
|
||||
* @param {string} relation - Relation name to check
|
||||
* @returns {boolean} True if relation is configured
|
||||
*/
|
||||
isRelationConfigured(relation) {
|
||||
return this.arbiter.relationConfigs.has(relation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relation configuration
|
||||
* @param {string} relation - Relation name
|
||||
* @returns {Object|null} Relation configuration or null
|
||||
*/
|
||||
getRelationConfig(relation) {
|
||||
return this.arbiter.relationConfigs.get(relation) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured relations
|
||||
* @returns {Map} Map of all relation configurations
|
||||
*/
|
||||
getAllRelationConfigs() {
|
||||
return this.arbiter.relationConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export compiled program to JSON
|
||||
* @param {string} programName - Name of the program to export
|
||||
* @returns {string|null} JSON string or null if program not found
|
||||
*/
|
||||
exportProgram(programName) {
|
||||
const program = this.getCompiledProgram(programName);
|
||||
if (!program) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
name: programName,
|
||||
compiledAt: program.compiledAt,
|
||||
program: this.serializeProgram(program.program),
|
||||
generatedRules: Array.from(program.generatedRules.entries())
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import compiled program from JSON
|
||||
* @param {string} jsonString - JSON string to import
|
||||
* @returns {boolean} True if imported successfully
|
||||
*/
|
||||
importProgram(jsonString) {
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
const program = this.deserializeProgram(data.program);
|
||||
|
||||
this.compiledPrograms.set(data.name, {
|
||||
program: program,
|
||||
generatedRules: new Map(data.generatedRules),
|
||||
compiledAt: new Date(data.compiledAt)
|
||||
});
|
||||
|
||||
// Apply rules to arbiter
|
||||
data.generatedRules.forEach(([relation, config]) => {
|
||||
this.arbiter.setRelationConfig(relation, config);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize program to plain object
|
||||
* @param {ProgramNode} program - Program to serialize
|
||||
* @returns {Object} Serialized program
|
||||
*/
|
||||
serializeProgram(program) {
|
||||
// This is a simplified serialization - in a real implementation,
|
||||
// you'd want to properly serialize all node types
|
||||
return {
|
||||
type: 'Program',
|
||||
definitions: program.definitions.map(def => ({
|
||||
type: 'Definition',
|
||||
name: def.name,
|
||||
definitionType: def.definitionType,
|
||||
fields: def.fields.map(field => ({
|
||||
type: 'Field',
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
isArray: field.isArray,
|
||||
isOptional: field.isOptional
|
||||
}))
|
||||
})),
|
||||
facts: program.facts.map(fact => ({
|
||||
type: 'Fact',
|
||||
name: fact.name,
|
||||
parameters: fact.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
}))
|
||||
})),
|
||||
evidence: program.evidence.map(ev => ({
|
||||
type: 'Evidence',
|
||||
name: ev.name,
|
||||
parameters: ev.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
})),
|
||||
returnType: ev.returnType
|
||||
})),
|
||||
measures: program.measures.map(measure => ({
|
||||
type: 'Measure',
|
||||
name: measure.name,
|
||||
parameters: measure.parameters.map(param => ({
|
||||
type: 'Parameter',
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
isArray: param.isArray
|
||||
})),
|
||||
returnType: measure.returnType
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize program from plain object
|
||||
* @param {Object} data - Serialized program data
|
||||
* @returns {ProgramNode} Deserialized program
|
||||
*/
|
||||
deserializeProgram(data) {
|
||||
// This is a simplified deserialization - in a real implementation,
|
||||
// you'd want to properly deserialize all node types
|
||||
const program = new ProgramNode();
|
||||
|
||||
// Note: This is a basic implementation. In practice, you'd need
|
||||
// to properly reconstruct all the AST nodes from the serialized data
|
||||
|
||||
return program;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* 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]*
|
||||
/ "/*" (!"*/" .)* "*/"
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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
|
||||
= "/*" (!"*/" .)* "*/"
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
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()})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { BaseNode } from './BaseNode.js';
|
||||
|
||||
/**
|
||||
* AST node for fusion statements
|
||||
* Represents: fusion max { ... }
|
||||
*/
|
||||
export class FusionNode extends BaseNode {
|
||||
constructor(strategy, location = null) {
|
||||
super('Fusion', location);
|
||||
this.strategy = strategy; // 'max', 'min', 'majority', 'average', etc.
|
||||
this.evidence = []; // Array of evidence statements
|
||||
this.weights = null; // Optional weights array
|
||||
}
|
||||
|
||||
/**
|
||||
* Add evidence to this fusion
|
||||
* @param {BaseNode} evidence - Evidence to add
|
||||
*/
|
||||
addEvidence(evidence) {
|
||||
this.evidence.push(evidence);
|
||||
this.addChild(evidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set weights for this fusion
|
||||
* @param {number[]} weights - Weights array
|
||||
*/
|
||||
setWeights(weights) {
|
||||
this.weights = weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fusion strategy
|
||||
* @returns {string} Fusion strategy
|
||||
*/
|
||||
getStrategy() {
|
||||
return this.strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all evidence statements
|
||||
* @returns {BaseNode[]} Evidence statements
|
||||
*/
|
||||
getEvidence() {
|
||||
return this.evidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the weights for this fusion
|
||||
* @returns {number[]|null} Weights or null
|
||||
*/
|
||||
getWeights() {
|
||||
return this.weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this fusion has weights
|
||||
* @returns {boolean} True if has weights
|
||||
*/
|
||||
hasWeights() {
|
||||
return this.weights !== null && this.weights.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a max fusion
|
||||
* @returns {boolean} True if max fusion
|
||||
*/
|
||||
isMax() {
|
||||
return this.strategy === 'max';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a min fusion
|
||||
* @returns {boolean} True if min fusion
|
||||
*/
|
||||
isMin() {
|
||||
return this.strategy === 'min';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a majority fusion
|
||||
* @returns {boolean} True if majority fusion
|
||||
*/
|
||||
isMajority() {
|
||||
return this.strategy === 'majority';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an average fusion
|
||||
* @returns {boolean} True if average fusion
|
||||
*/
|
||||
isAverage() {
|
||||
return this.strategy === 'average';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of evidence statements
|
||||
* @returns {number} Number of evidence statements
|
||||
*/
|
||||
getEvidenceCount() {
|
||||
return this.evidence.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the fusion
|
||||
* @returns {string[]} Array of error messages
|
||||
*/
|
||||
validate() {
|
||||
const errors = [];
|
||||
|
||||
// Validate strategy
|
||||
const validStrategies = [
|
||||
'max',
|
||||
'min',
|
||||
'majority',
|
||||
'average',
|
||||
'sum',
|
||||
'sum_unbounded',
|
||||
'median',
|
||||
'optimistic',
|
||||
'pessimistic',
|
||||
'top2',
|
||||
'top3',
|
||||
'priority',
|
||||
'custom',
|
||||
'count'
|
||||
];
|
||||
if (!validStrategies.includes(this.strategy)) {
|
||||
errors.push(`Invalid fusion strategy: ${this.strategy}`);
|
||||
}
|
||||
|
||||
// Validate evidence
|
||||
if (this.evidence.length === 0) {
|
||||
errors.push('Fusion must have at least one evidence statement');
|
||||
}
|
||||
|
||||
// Validate each evidence statement
|
||||
this.evidence.forEach((ev, index) => {
|
||||
const evErrors = ev.validate ? ev.validate() : [];
|
||||
errors.push(...evErrors.map(err => `Evidence ${index + 1}: ${err}`));
|
||||
});
|
||||
|
||||
// Validate weights
|
||||
if (this.weights !== null) {
|
||||
if (!Array.isArray(this.weights)) {
|
||||
errors.push('Weights must be an array');
|
||||
} else if (this.weights.length !== this.evidence.length) {
|
||||
errors.push('Weights array length must match evidence count');
|
||||
} else if (this.weights.some(w => typeof w !== 'number' || w < 0)) {
|
||||
errors.push('All weights must be non-negative numbers');
|
||||
} else if (this.strategy === 'custom') {
|
||||
const total = this.weights.reduce((sum, w) => sum + w, 0);
|
||||
if (Math.abs(total - 1.0) > 1e-6) {
|
||||
errors.push('Custom weights must sum to 1.0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.strategy === 'custom' && (!this.weights || this.weights.length === 0)) {
|
||||
errors.push('Custom fusion requires weights');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
toString() {
|
||||
const weightsStr = this.hasWeights() ? ` weights[${this.weights.length}]` : '';
|
||||
return `Fusion(${this.strategy}, ${this.evidence.length} evidence${weightsStr})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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(', ')})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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()})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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()})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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)`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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()})`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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
@@ -0,0 +1,114 @@
|
||||
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'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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('.');
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
Reference in New Issue
Block a user