import { ProgramNode, DefinitionNode, FactNode, EvidenceNode, MeasureNode, DirectEvidenceNode, PatternMatchNode, DefeasibleLogicNode, FusionNode, PredicateNode, ExpressionNode } from '../nodes/index.js'; /** * Rule Generator for converting AST to setRelationConfig calls * Generates rule configurations that interface with the existing rule system */ export class RuleGenerator { constructor(arbiter) { this.arbiter = arbiter; this.generatedRules = new Map(); this.errors = []; this.dependencyIndex = new Map(); } /** * Generate rules from AST program * @param {ProgramNode} program - AST program to generate rules from * @returns {Object} Generation result with success status and errors */ generateRules(program) { this.errors = []; this.generatedRules.clear(); this.dependencyIndex.clear(); try { // Generate rules for each evidence definition program.evidence.forEach(evidence => { this.generateEvidenceRules(evidence); }); // Generate rules for each measure definition program.measures.forEach(measure => { this.generateMeasureRules(measure); }); // Generate fact relation configs (requires injection at check time) program.facts.forEach(fact => { this.generateFactConfig(fact); }); // Apply generated rules to arbiter this.applyRulesToArbiter(); return { success: this.errors.length === 0, errors: this.errors, generatedCount: this.generatedRules.size }; } catch (error) { this.errors.push(`Generation error: ${error.message}`); return { success: false, errors: this.errors, generatedCount: 0 }; } } /** * Generate rules for an evidence definition * @param {EvidenceNode} evidence - Evidence to generate rules for */ generateEvidenceRules(evidence) { const relationName = evidence.name; const ruleConfig = this.buildRuleConfig(evidence); if (ruleConfig) { this._annotateDependencies(relationName, ruleConfig); this.generatedRules.set(relationName, ruleConfig); } } /** * Generate rules for a measure definition * @param {MeasureNode} measure - Measure to generate rules for */ generateMeasureRules(measure) { const relationName = measure.name; const ruleConfig = this.buildMeasureRuleConfig(measure); if (ruleConfig) { this._annotateDependencies(relationName, ruleConfig); this.generatedRules.set(relationName, ruleConfig); } } /** * Generate relation config for a fact declaration. * Facts require injection at check time (e.g. from user DB, session store). * Registered as type: 'direct' with requiresInjection flag so the gate-check * pipeline can pre-fetch facts before calling graphStore.check(). * @param {FactNode} fact - Fact to generate config for */ generateFactConfig(fact) { const name = fact.name; const params = fact.params || []; const paramNames = params.map(p => p.name); const paramTypes = params.map(p => p.type); this.generatedRules.set(name, { type: 'direct', relation: name, isFactRelation: true, requiresInjection: true, arity: paramTypes.length, paramTypes: paramTypes, paramNames: paramNames, cacheDirective: fact.cacheDirective || fact.cache || 'lazy', properties: Object.fromEntries( (fact.properties || []).map(p => typeof p === 'string' ? [p, true] : Array.isArray(p) ? p : [p, true] ) ) }); } _annotateDependencies(relationName, ruleConfig) { if (!ruleConfig || typeof ruleConfig !== 'object') return; const dependsOn = new Set(); const dependsByLevel = { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set() }; const collect = (rule, targetSet) => { if (!rule || typeof rule !== 'object') return; if (rule.type === 'direct' && rule.relation) { targetSet.add(rule.relation); } if (rule.type === 'tuple_to_userset') { if (rule.tuplesetRelation) targetSet.add(rule.tuplesetRelation); if (rule.computedRelation) targetSet.add(rule.computedRelation); } if (rule.type === 'parent' && rule.parentRelation) { targetSet.add(rule.parentRelation); } if (rule.type === 'multi_hop' && rule.relation) { targetSet.add(rule.relation); } if (rule.type === 'chain' && Array.isArray(rule.steps)) { for (const step of rule.steps) { if (typeof step === 'string') targetSet.add(step); else if (step && typeof step.relation === 'string') targetSet.add(step.relation); } } if (rule.type === 'relational_comparator') { if (rule.left?.valueRelation) targetSet.add(rule.left.valueRelation); if (rule.right?.valueRelation) targetSet.add(rule.right.valueRelation); if (rule.left?.rule) collect(rule.left.rule, targetSet); if (rule.right?.rule) collect(rule.right.rule, targetSet); } const logicalKeys = ['union', 'intersection', 'exclusion', 'never', 'always', 'requires', 'when', 'unless']; for (const key of logicalKeys) { const node = rule[key]; const ruleList = node?.rules || node?.union?.rules || node?.intersection?.rules; if (Array.isArray(ruleList)) { for (const child of ruleList) collect(child, targetSet); } if (node?.rule) collect(node.rule, targetSet); } }; const collectWithKeys = (rule, keys, targetSet) => { if (!rule || typeof rule !== 'object') return; for (const key of keys) { const node = rule[key]; const ruleList = node?.rules || node?.union?.rules || node?.intersection?.rules; if (Array.isArray(ruleList)) { for (const child of ruleList) collect(child, targetSet); } if (node?.rule) collect(node.rule, targetSet); } }; if (ruleConfig.type === 'logical') { if (ruleConfig.never) collect(ruleConfig.never, dependsByLevel.never); if (ruleConfig.always) collect(ruleConfig.always, dependsByLevel.always); if (ruleConfig.requires) collect(ruleConfig.requires, dependsByLevel.requires); if (ruleConfig.when) collect(ruleConfig.when, dependsByLevel.when); if (ruleConfig.unless) collect(ruleConfig.unless, dependsByLevel.unless); collectWithKeys(ruleConfig, ['union', 'intersection', 'exclusion'], dependsByLevel.ordinary); } else { collect(ruleConfig, dependsOn); } for (const level of Object.keys(dependsByLevel)) { for (const rel of dependsByLevel[level]) { dependsOn.add(rel); } } const orderedDependsOn = Array.from(dependsOn); if (orderedDependsOn.length > 0) { ruleConfig.dependsOn = orderedDependsOn; } if (Object.values(dependsByLevel).some(set => set.size > 0)) { ruleConfig.dependsByLevel = { never: Array.from(dependsByLevel.never), always: Array.from(dependsByLevel.always), requires: Array.from(dependsByLevel.requires), when: Array.from(dependsByLevel.when), unless: Array.from(dependsByLevel.unless), ordinary: Array.from(dependsByLevel.ordinary) }; } if (orderedDependsOn.length > 0) { for (const rel of orderedDependsOn) { let entry = this.dependencyIndex.get(rel); if (!entry) { entry = { all: new Set(), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set() } }; this.dependencyIndex.set(rel, entry); } entry.all.add(relationName); if (ruleConfig.dependsByLevel) { for (const level of Object.keys(entry.byLevel)) { if (ruleConfig.dependsByLevel[level]?.includes(rel)) { entry.byLevel[level].add(relationName); } } } } } } getDependencyIndex() { return this.dependencyIndex; } /** * Build rule configuration from evidence * @param {EvidenceNode} evidence - Evidence to build config for * @returns {Object|null} Rule configuration or null */ buildRuleConfig(evidence) { if (!evidence.body || !evidence.body.statements) { this.errors.push(`Evidence ${evidence.name} has no body`); return null; } const statements = evidence.body.statements; // Handle single statement evidence if (statements.length === 1) { return this.buildSingleStatementRule(statements[0]); } // Handle multiple statements with logical operators return this.buildLogicalRule(statements); } /** * Build rule configuration for a single statement * @param {BaseNode} statement - Statement to build rule for * @returns {Object|null} Rule configuration or null */ buildSingleStatementRule(statement) { switch (statement.type) { case 'DirectEvidence': return this.buildDirectRule(statement); case 'PatternMatch': return this.buildPatternMatchRule(statement); case 'DefeasibleLogic': return this.buildDefeasibleRule(statement); case 'Fusion': return this.buildFusionRule(statement); case 'PredicateCall': return this.buildPredicateRule(statement); case 'UnaryExpression': return this.buildUnaryRule(statement); case 'BinaryExpression': // Top-level comparator — emit a relational_comparator rule. RF-24 closure. return this.buildRuleFromExpressionNode(statement); case 'Expression': // Handle expressions that might be predicate calls if (statement.type === 'PredicateCall') { return this.buildPredicateRule(statement); } return this.buildRuleFromExpressionNode(statement); default: this.errors.push(`Unsupported statement type: ${statement.type}`); return null; } } /** * Build rule for unary expression (NOT) * @param {Object} expression - Unary expression * @returns {Object|null} Rule configuration or null */ buildUnaryRule(expression) { if (expression.operator !== 'NOT') { this.errors.push(`Unsupported unary operator: ${expression.operator}`); return null; } const innerRule = this.buildRuleFromExpression(expression.operand); if (!innerRule) { return null; } // NOT x: evaluation will negate the inner rule's possibility (1 - poss) return { type: 'logical', intersection: { rules: [innerRule], aggregator: 'min', negate: true } }; } /** * Build logical rule configuration for multiple statements * @param {BaseNode[]} statements - Statements to combine * @returns {Object|null} Rule configuration or null */ buildLogicalRule(statements) { const rules = []; statements.forEach(statement => { const rule = this.buildSingleStatementRule(statement); if (rule) { rules.push(rule); } }); if (rules.length === 0) { this.errors.push('No valid rules found in evidence body'); return null; } if (rules.length === 1) { return rules[0]; } // Combine rules with intersection (AND) logic — REBAC default. // Use explicit fusion max { ... } in DSL for OR semantics. return { type: 'logical', intersection: { rules: rules, aggregator: 'min' } }; } /** * Build direct rule configuration * @param {DirectEvidenceNode} directEvidence - Direct evidence statement * @returns {Object|null} Rule configuration or null */ buildDirectRule(directEvidence) { if (!directEvidence.predicate) { this.errors.push('Direct evidence must have a predicate'); return null; } const predicate = directEvidence.predicate; const relation = predicate.name; return { type: 'direct', relation: relation, reverse: false }; } /** * Build pattern match rule configuration * @param {PatternMatchNode} patternMatch - Pattern match statement * @returns {Object|null} Rule configuration or null */ buildPatternMatchRule(patternMatch) { if (!patternMatch.predicate) { this.errors.push('Pattern match must have a predicate'); return null; } const predicate = patternMatch.predicate; const relation = predicate.name; // Membership/hierarchy predicates map to TupleToUsersetRule / ParentRule // regardless of body shape — those have priority over chain detection. if (this.isMembershipPredicate(predicate)) { return this.buildTupleToUsersetRule(patternMatch); } if (this.isHierarchyPredicate(predicate)) { return this.buildParentRule(patternMatch); } // Chain detection: ADR-000 ChainRule shape is "works_in(p, *d) { has_access(d, r) }". // The outer PatternMatch has a Wildcard binding, and its body contains a single // PredicateCall (no DefeasibleLogic wrapping, no nested PatternMatch). Treat that // as a chain: two-hop traversal through the wildcard intermediate. RF-24 closure // (parallel to RF-22/RF-23 — DSL→engine mapping gap surfaced by rigor coverage). if (this._isChainPattern(patternMatch)) { return this.buildChainRule(patternMatch); } return this.buildMultiHopRule(patternMatch); } /** * Detect the ChainRule shape: a PatternMatch whose body contains exactly one * PredicateCall and uses a Wildcard arg to bind the intermediate. The predicate * name itself is NOT in the membership/hierarchy lists (those map to TUS/Parent). */ _isChainPattern(patternMatch) { if (!patternMatch.body || !Array.isArray(patternMatch.body.statements)) return false; const stmts = patternMatch.body.statements; if (stmts.length !== 1) return false; if (stmts[0].type !== 'PredicateCall') return false; const predicate = patternMatch.predicate; if (!predicate || !Array.isArray(predicate.args)) return false; // Must use at least one Wildcard (*var) to bind the intermediate return predicate.args.some(arg => arg && arg.type === 'Wildcard'); } /** * Build chain rule configuration (ADR-000 ChainRule). * Compiles "works_in(p, *d) { has_access(d, doc) }" into * { type: 'chain', steps: ['works_in', 'has_access'] } * The intermediate wildcard binds the two predicates' arguments. */ buildChainRule(patternMatch) { const steps = []; steps.push(patternMatch.predicate.name); const inner = patternMatch.body.statements[0]; if (inner && inner.type === 'PredicateCall' && inner.name) { steps.push(inner.name); } return { type: 'chain', steps, aggregator: 'max', collectValues: true }; } /** * Build tuple-to-userset rule configuration * @param {PatternMatchNode} patternMatch - Pattern match statement * @returns {Object|null} Rule configuration or null */ buildTupleToUsersetRule(patternMatch) { const predicate = patternMatch.predicate; const relation = predicate.name; return { type: 'tuple_to_userset', tuplesetRelation: 'owner', // Default, could be inferred from context computedRelation: relation, reverse: false, earlyExitThreshold: 0.95, maxIntermediates: patternMatch.limit || 10 }; } /** * Build parent rule configuration * @param {PatternMatchNode} patternMatch - Pattern match statement * @returns {Object|null} Rule configuration or null */ buildParentRule(patternMatch) { const predicate = patternMatch.predicate; const relation = predicate.name; return { type: 'parent', parentRelation: 'parent', // Default, could be inferred from context relation: relation, reverse: false, aggregator: 'max' }; } /** * Build multi-hop rule configuration * @param {PatternMatchNode} patternMatch - Pattern match statement * @returns {Object|null} Rule configuration or null */ buildMultiHopRule(patternMatch) { const predicate = patternMatch.predicate; const relation = predicate.name; return { type: 'multi_hop', relation: relation, maxDepth: 3, pathAggregation: 'max', reverse: false, fallbackToBasicPaths: true, collectValues: false }; } /** * Build defeasible rule configuration * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildDefeasibleRule(defeasibleLogic) { const logicType = defeasibleLogic.logicType; if (logicType === 'NEVER') { return this.buildNeverRule(defeasibleLogic); } else if (logicType === 'ALWAYS') { return this.buildStrictRule(defeasibleLogic); } else if (logicType === 'WHEN') { return this.buildDefeasibleRuleWithDefeater(defeasibleLogic); } else if (logicType === 'UNLESS') { return this.buildDefeaterRule(defeasibleLogic); } else if (logicType === 'REQUIRES') { return this.buildRequirementRule(defeasibleLogic); } this.errors.push(`Unsupported defeasible logic type: ${logicType}`); return null; } /** * Build NEVER rule configuration (absolute denial) * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildNeverRule(defeasibleLogic) { const condition = this.buildRuleFromExpression(defeasibleLogic.condition); return { type: 'logical', never: { union: { rules: [condition], aggregator: 'max' } } }; } /** * Build strict rule configuration * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildStrictRule(defeasibleLogic) { const condition = this.buildRuleFromExpression(defeasibleLogic.condition); return { type: 'logical', always: { direct: condition, aggregator: 'min' } }; } /** * Build defeasible rule with defeater * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildDefeasibleRuleWithDefeater(defeasibleLogic) { const condition = this.buildRuleFromExpression(defeasibleLogic.condition); const defeater = this.buildRuleFromExpression(defeasibleLogic.defeater); const rule = { type: 'logical', when: { intersection: { rules: [condition], aggregator: 'min' } } }; if (defeater) { rule.unless = { union: { rules: [defeater], aggregator: 'max' } }; } return rule; } /** * Build defeater rule configuration * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildDefeaterRule(defeasibleLogic) { const condition = this.buildRuleFromExpression(defeasibleLogic.condition); return { type: 'logical', unless: { union: { rules: [condition], aggregator: 'max' } } }; } /** * Build requirement rule configuration * @param {DefeasibleLogicNode} defeasibleLogic - Defeasible logic statement * @returns {Object|null} Rule configuration or null */ buildRequirementRule(defeasibleLogic) { const condition = this.buildRuleFromExpression(defeasibleLogic.condition); return { type: 'logical', requires: { union: { rules: [condition], aggregator: 'min' } } }; } /** * Build fusion rule configuration * @param {FusionNode} fusion - Fusion statement * @returns {Object|null} Rule configuration or null */ buildFusionRule(fusion) { const rules = []; (fusion.expressions || fusion.evidence || []).forEach(evidence => { const rule = this.buildRuleFromExpression(evidence); if (rule) { rules.push(rule); } }); if (rules.length === 0) { this.errors.push('Fusion has no valid evidence'); return null; } if (fusion.weights && fusion.strategy !== 'custom') { this.errors.push(`Fusion weights require custom strategy, got: ${fusion.strategy}`); return null; } if (fusion.strategy === 'custom') { if (!fusion.weights || fusion.weights.length === 0) { this.errors.push('Custom fusion requires weights'); return null; } const total = fusion.weights.reduce((sum, w) => sum + w, 0); if (Math.abs(total - 1.0) > 1e-6) { this.errors.push('Custom fusion weights must sum to 1.0'); return null; } } const union = { rules: rules, aggregator: fusion.strategy }; if (fusion.weights && fusion.weights.length > 0) { union.owaWeights = fusion.weights; } return { type: 'logical', union }; } /** * Build rule from expression * @param {BaseNode} expression - Expression to build rule from * @returns {Object|null} Rule configuration or null */ buildRuleFromExpression(expression) { if (!expression) { return null; } if (expression.type === 'Predicate') { return this.buildDirectRuleFromPredicate(expression); } else if (expression.type === 'Expression') { return this.buildRuleFromExpressionNode(expression); } else if (expression.type === 'PredicateCall') { return this.buildPredicateRule(expression); } else if (expression.type === 'UnaryExpression') { return this.buildUnaryRule(expression); } this.errors.push(`Unsupported expression type: ${expression.type}`); return null; } /** * Build direct rule from predicate * @param {PredicateNode} predicate - Predicate to build rule from * @returns {Object|null} Rule configuration or null */ buildDirectRuleFromPredicate(predicate) { const expanded = this._expandPredicate(predicate.name); if (expanded) return expanded; return { type: 'direct', relation: predicate.name, reverse: false }; } _expandPredicate(predicateName) { const existingConfig = this.generatedRules.get(predicateName) || this.arbiter?.relationConfigs?.get(predicateName); if (!existingConfig) return null; if (!existingConfig.union && !existingConfig.intersection && !existingConfig.exclusion) return null; const logicalKey = existingConfig.union ? 'union' : existingConfig.intersection ? 'intersection' : 'exclusion'; const subRules = Array.isArray(existingConfig[logicalKey]?.rules) ? existingConfig[logicalKey].rules : Array.isArray(existingConfig[logicalKey]) ? existingConfig[logicalKey] : []; if (subRules.length === 0) return null; const expandedRules = subRules.map(r => { if (r && r.type === 'direct') return { type: 'direct', relation: r.relation, reverse: !!r.reverse }; if (typeof r === 'string') return { type: 'direct', relation: r, reverse: false }; return null; }).filter(Boolean); if (expandedRules.length === 0) return null; return { type: 'logical', [logicalKey]: { rules: expandedRules, aggregator: existingConfig[logicalKey]?.aggregator || 'min' }, // Flag to tell the evaluator: this expanded sub-predicate is unary — // use the subject as the object instead of inheriting the parent's object. _subjectAsObject: true }; } /** * Build rule from expression node * @param {ExpressionNode} expression - Expression to build rule from * @returns {Object|null} Rule configuration or null */ buildRuleFromExpressionNode(expression) { if (expression.type === 'AttributeAccess') { return this.buildAttributeRule(expression); } else if (expression.type === 'PredicateCall') { return this.buildPredicateRule(expression); } else if (expression.type === 'BinaryExpression' && expression.operator === 'within') { return this.buildWithinRule(expression); } else if (expression.type === 'BinaryExpression' && this._isComparatorOperator(expression.operator)) { // ADR-000 RelationalComparatorRule shape: "userRisk(u) <= riskLimit(r)". // Route BinaryExpression with comparator operators here so the // evaluator can run a fuzzy interval comparison instead of treating // them as logical truth values. RF-24 closure. return this.buildRelationalComparatorRule(expression); } this.errors.push(`Unsupported expression type: ${expression.type}`); return null; } /** * Detect comparator operators per ADR-000. These are the operators that map * to RelationalComparatorRule (vs. the boolean operators like `&&`/`||` which * keep going through LogicalOperators). */ _isComparatorOperator(operator) { return operator === '>' || operator === '>=' || operator === '<' || operator === '<=' || operator === '==' || operator === '!='; } /** * Build relational comparator rule configuration (ADR-000 RelationalComparatorRule). * Compiles "personAge(p) >= docMinAge(d)" into * { * type: 'relational_comparator', * left: { rule: , extractValue: true }, * right: { rule: , extractValue: true }, * comparator: '>=', * marginOfSafety: 1.0, * fallbackBehavior: 'deny', * minRulePossibility: 0 * } */ buildRelationalComparatorRule(binaryExpression) { const comparator = binaryExpression.operator; const left = this._buildComparatorOperand(binaryExpression.left); const right = this._buildComparatorOperand(binaryExpression.right); if (!left || !right) { this.errors.push(`Comparator operands must resolve to predicate calls (operator=${comparator})`); return null; } return { type: 'relational_comparator', left, right, comparator, marginOfSafety: 1.0, fallbackBehavior: 'deny', minRulePossibility: 0 }; } /** * Wrap a BinaryExpression side into a relational_comparator operand. The * operand's `rule` field is the original predicate call (preserving reference * semantics so the inner rule's evaluator can resolve its values). `extractValue` * tells the evaluator to read the relation's `value` field rather than its * `possibility`, which is what `personAge(p)` / `docMinAge(d)` semantics require. */ _buildComparatorOperand(side) { if (!side) return null; if (side.type === 'PredicateCall') { return { rule: side, extractValue: true, evaluatorFrom: 'auto' }; } if (side.type === 'AttributeAccess') { // user.age — treat the attribute path as a "measure" reference return { rule: side, extractValue: true, evaluatorFrom: 'auto', attributePath: side.getAttributePath ? side.getAttributePath() : null }; } return null; } /** * Build attribute rule configuration * @param {ExpressionNode} expression - Attribute expression * @returns {Object|null} Rule configuration or null */ buildAttributeRule(expression) { const attributePath = expression.getAttributePath(); return { type: 'direct', relation: attributePath, reverse: false }; } /** * Build function rule configuration * @param {ExpressionNode} expression - Function expression * @returns {Object|null} Rule configuration or null */ buildPredicateRule(expression) { const predicateName = expression.name; if (expression.challenge) { return this.buildChallengeRule(expression, null); } // Expand composite (logical) predicate references into their direct // leaf components so the optimizer can flatten to a correct direct_list. const expanded = this._expandPredicate(predicateName); if (expanded) return expanded; return { type: 'direct', relation: predicateName, reverse: false }; } buildWithinRule(expression) { const left = expression.left; const right = expression.right; if (left && left.type === 'PredicateCall' && left.challenge) { this.errors.push('within must be applied to a challenge proof field, e.g. !mfa(user).issued_at within 10m'); return null; } if (left && left.type === 'AttributeAccess' && left.object?.type === 'PredicateCall' && left.object?.challenge) { if (left.attribute !== 'issued_at' && left.attribute !== 'issuedAt') { this.errors.push('only issued_at is supported for challenge recency checks'); return null; } return this.buildChallengeRule(left.object, right); } this.errors.push('within operator currently only supported with challenge predicates'); return null; } buildChallengeRule(expression, duration) { const predicateName = expression.name; const subject = this._resolveChallengeSubject(expression.args || []); const withinMs = duration ? this._durationToMs(duration) : null; return { type: 'challenge', challenge: predicateName, subject, ...(withinMs !== null && { withinMs }) }; } _resolveChallengeSubject(args) { if (!args || !args.length) return 'user'; const first = args[0]; if (first?.type === 'Variable') { if (first.name === 'object') return 'object'; if (first.name === 'session') return 'session'; return 'user'; } return 'user'; } _durationToMs(duration) { if (!duration || duration.value === undefined) return null; const raw = typeof duration.value === 'string' ? duration.value : String(duration.value); const unit = duration.unit || raw.slice(-1); const numeric = parseFloat(raw); if (!Number.isFinite(numeric)) return null; switch (unit) { case 's': return numeric * 1000; case 'm': return numeric * 60 * 1000; case 'h': return numeric * 60 * 60 * 1000; case 'd': return numeric * 24 * 60 * 60 * 1000; case 'w': return numeric * 7 * 24 * 60 * 60 * 1000; default: return null; } } /** * Build measure rule configuration * @param {MeasureNode} measure - Measure to build rule for * @returns {Object|null} Rule configuration or null */ buildMeasureRuleConfig(measure) { if (!measure.body) { this.errors.push(`Measure ${measure.name} has no body`); return null; } // Measures typically generate computed rules return { type: 'computed', relation: measure.name }; } /** * Check if predicate is membership-based * @param {PredicateNode} predicate - Predicate to check * @returns {boolean} True if membership predicate */ isMembershipPredicate(predicate) { const membershipPredicates = ['member', 'member_of', 'belongs_to', 'is_member']; return membershipPredicates.includes(predicate.name.toLowerCase()); } /** * Check if predicate is hierarchy-based * @param {PredicateNode} predicate - Predicate to check * @returns {boolean} True if hierarchy predicate */ isHierarchyPredicate(predicate) { const hierarchyPredicates = ['parent', 'parent_of', 'child', 'child_of', 'ancestor', 'descendant']; return hierarchyPredicates.includes(predicate.name.toLowerCase()); } /** * Apply generated rules to arbiter */ applyRulesToArbiter() { if (!this.arbiter || typeof this.arbiter.setRelationConfig !== 'function') { // Skip applying rules if arbiter is not available or doesn't support setRelationConfig return; } this.generatedRules.forEach((config, relation) => { try { this.arbiter.setRelationConfig(relation, config); } catch (error) { this.errors.push(`Failed to set relation config for ${relation}: ${error.message}`); } }); if (typeof this.arbiter.registerDependencyIndex === 'function') { this.arbiter.registerDependencyIndex(this.dependencyIndex); } } /** * Get generated rules * @returns {Map} Map of generated rules */ getGeneratedRules() { return this.generatedRules; } /** * Get generation errors * @returns {string[]} Array of error messages */ getErrors() { return this.errors; } }