qnce-engine
Version:
Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization
195 lines • 6.92 kB
JavaScript
;
// QNCE Condition Evaluator - Sprint 3.4
// Parses and evaluates conditional expressions for choice visibility
Object.defineProperty(exports, "__esModule", { value: true });
exports.conditionEvaluator = exports.ConditionEvaluator = exports.ConditionEvaluationError = void 0;
/**
* Error thrown when condition evaluation fails
*/
class ConditionEvaluationError extends Error {
expression;
cause;
constructor(message, expression, cause) {
super(message);
this.expression = expression;
this.cause = cause;
this.name = 'ConditionEvaluationError';
}
}
exports.ConditionEvaluationError = ConditionEvaluationError;
/**
* Built-in expression operators
*/
const OPERATORS = {
'>=': (a, b) => a >= b,
'<=': (a, b) => a <= b,
'>': (a, b) => a > b,
'<': (a, b) => a < b,
'==': (a, b) => a == b,
'===': (a, b) => a === b,
'!=': (a, b) => a != b,
'!==': (a, b) => a !== b,
'&&': (a, b) => a && b,
'||': (a, b) => a || b,
};
/**
* Condition evaluator service for parsing and executing conditional expressions
*/
class ConditionEvaluator {
customEvaluator;
// Cache compiled functions for better performance
functionCache = new Map();
maxCacheSize = 100;
/**
* Set a custom evaluator function for handling complex conditions
*/
setCustomEvaluator(evaluator) {
this.customEvaluator = evaluator;
}
/**
* Clear the custom evaluator
*/
clearCustomEvaluator() {
this.customEvaluator = undefined;
}
/**
* Evaluate a condition expression against the provided context
*/
evaluate(expression, context) {
try {
// If custom evaluator is set, use it first
if (this.customEvaluator) {
return this.customEvaluator(expression, context);
}
// Use built-in evaluator
return this.evaluateBuiltIn(expression, context);
}
catch (error) {
throw new ConditionEvaluationError(`Failed to evaluate condition: ${expression}`, expression, error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Built-in expression evaluator with support for common patterns
*/
evaluateBuiltIn(expression, context) {
// Handle empty expressions
if (!expression || expression.trim() === '') {
throw new ConditionEvaluationError('Empty or whitespace-only condition expression', expression);
}
// Sanitize and prepare expression
const sanitizedExpression = this.sanitizeExpression(expression);
// Handle simple cases first
if (sanitizedExpression === 'true')
return true;
if (sanitizedExpression === 'false')
return false;
// Create safe evaluation context
const evalContext = this.createEvaluationContext(context);
// Check function cache for performance
let func = this.functionCache.get(sanitizedExpression);
if (!func) {
try {
// Use Function constructor for safe evaluation (better than eval)
func = new Function('flags', 'state', 'timestamp', 'customData', `
"use strict";
return ${sanitizedExpression};
`);
// Cache the function for future use
if (this.functionCache.size >= this.maxCacheSize) {
// Remove oldest entry when cache is full
const firstKey = this.functionCache.keys().next().value;
if (firstKey) {
this.functionCache.delete(firstKey);
}
}
this.functionCache.set(sanitizedExpression, func);
}
catch (error) {
throw new ConditionEvaluationError(`Invalid expression syntax: ${expression}`, expression, error instanceof Error ? error : new Error(String(error)));
}
}
try {
return !!func(evalContext.flags, evalContext.state, evalContext.timestamp, evalContext.customData);
}
catch (error) {
throw new ConditionEvaluationError(`Runtime error evaluating: ${expression}`, expression, error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Sanitize expression to prevent code injection
*/
sanitizeExpression(expression) {
// Remove potentially dangerous patterns
const dangerous = [
/\beval\b/g,
/\bFunction\b/g,
/\bconstructor\b/g,
/\bprototype\b/g,
/\b__proto__\b/g,
/\bimport\b/g,
/\brequire\b/g,
/\bprocess\b/g,
/\bglobal\b/g,
/\bwindow\b/g,
/\bdocument\b/g,
];
let sanitized = expression.trim();
for (const pattern of dangerous) {
if (pattern.test(sanitized)) {
throw new ConditionEvaluationError(`Potentially unsafe expression detected: ${expression}`, expression);
}
}
return sanitized;
}
/**
* Create a safe evaluation context from the condition context
*/
createEvaluationContext(context) {
return {
flags: { ...context.state.flags },
state: {
currentNodeId: context.state.currentNodeId,
flags: { ...context.state.flags },
history: [...context.state.history],
},
timestamp: context.timestamp,
customData: context.customData ? { ...context.customData } : {},
};
}
/**
* Validate that an expression is syntactically correct without evaluating it
*/
validateExpression(expression) {
try {
this.sanitizeExpression(expression);
// Try to create the function without executing it
new Function('flags', 'state', 'timestamp', 'customData', `
"use strict";
return ${expression};
`);
return { valid: true };
}
catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Get list of flag names referenced in an expression
*/
getReferencedFlags(expression) {
const flagPattern = /flags\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
const matches = [];
let match;
while ((match = flagPattern.exec(expression)) !== null) {
matches.push(match[1]);
}
return Array.from(new Set(matches)); // Remove duplicates
}
}
exports.ConditionEvaluator = ConditionEvaluator;
// Create a global instance for the engine to use
exports.conditionEvaluator = new ConditionEvaluator();
//# sourceMappingURL=condition.js.map