qnce-engine
Version:
Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization
261 lines • 10.1 kB
JavaScript
;
// QNCE Choice Validation System - Sprint 3.2
// Ensures only valid choices can be executed, providing robust error handling
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultChoiceValidator = exports.StandardValidationRules = void 0;
exports.createChoiceValidator = createChoiceValidator;
exports.createValidationContext = createValidationContext;
/**
* Built-in validation rules for common scenarios
*/
class StandardValidationRules {
/**
* Validates that choice exists in the current node's choices
*/
static CHOICE_EXISTS = {
name: 'choice-exists',
priority: 1,
validate(choice, context) {
const exists = context.currentNode.choices.some(c => c.text === choice.text && c.nextNodeId === choice.nextNodeId);
return {
isValid: exists,
reason: exists ? undefined : `Choice "${choice.text}" is not available from current node`,
suggestedChoices: exists ? undefined : context.currentNode.choices
};
}
};
/**
* Validates flag-based conditions for choice availability
*/
static FLAG_CONDITIONS = {
name: 'flag-conditions',
priority: 2,
validate(choice, context) {
// Check if choice has flag requirements
if (choice.flagRequirements) {
const missingFlags = [];
const conflictingFlags = [];
for (const [flagName, requiredValue] of Object.entries(choice.flagRequirements)) {
const currentValue = context.state.flags[flagName];
if (requiredValue === true && !currentValue) {
missingFlags.push(flagName);
}
else if (requiredValue === false && currentValue) {
conflictingFlags.push(flagName);
}
else if (typeof requiredValue !== 'boolean' && currentValue !== requiredValue) {
missingFlags.push(`${flagName}=${requiredValue}`);
}
}
if (missingFlags.length > 0 || conflictingFlags.length > 0) {
const reasons = [];
if (missingFlags.length > 0) {
reasons.push(`Missing required flags: ${missingFlags.join(', ')}`);
}
if (conflictingFlags.length > 0) {
reasons.push(`Conflicting flags: ${conflictingFlags.join(', ')}`);
}
return {
isValid: false,
reason: `Flag conditions not met: ${reasons.join('; ')}`,
failedConditions: [...missingFlags, ...conflictingFlags],
metadata: {
missingFlags,
conflictingFlags,
currentFlags: context.state.flags
}
};
}
}
return {
isValid: true,
reason: undefined
};
}
};
/**
* Validates choice is not disabled or marked as unavailable
*/
static CHOICE_ENABLED = {
name: 'choice-enabled',
priority: 3,
validate(choice, _context) {
if (choice.enabled === false) {
return {
isValid: false,
reason: `Choice "${choice.text}" is currently disabled`,
failedConditions: ['choice-disabled']
};
}
return {
isValid: true,
reason: undefined
};
}
};
/**
* Validates time-based requirements for choice availability
*/
static TIME_CONDITIONS = {
name: 'time-conditions',
priority: 4,
validate(choice, context) {
if (choice.timeRequirements) {
const now = new Date();
const currentTime = context.timestamp || now.getTime();
const failures = [];
if (choice.timeRequirements.availableAfter) {
const afterTime = new Date(choice.timeRequirements.availableAfter).getTime();
if (currentTime < afterTime) {
failures.push(`not available until ${choice.timeRequirements.availableAfter}`);
}
}
if (choice.timeRequirements.availableBefore) {
const beforeTime = new Date(choice.timeRequirements.availableBefore).getTime();
if (currentTime > beforeTime) {
failures.push(`no longer available after ${choice.timeRequirements.availableBefore}`);
}
}
if (choice.timeRequirements.minTime && currentTime < choice.timeRequirements.minTime) {
failures.push(`minimum time not reached`);
}
if (choice.timeRequirements.maxTime && currentTime > choice.timeRequirements.maxTime) {
failures.push(`maximum time exceeded`);
}
if (failures.length > 0) {
return {
isValid: false,
reason: `Time conditions not met: ${failures.join(', ')}`,
failedConditions: failures,
metadata: {
currentTime,
timeRequirements: choice.timeRequirements
}
};
}
}
return {
isValid: true,
reason: undefined
};
}
};
/**
* Validates inventory-based requirements for choice availability
*/
static INVENTORY_CONDITIONS = {
name: 'inventory-conditions',
priority: 5,
validate(choice, context) {
if (choice.inventoryRequirements) {
const inventory = context.state.flags.inventory || {};
const missingItems = [];
for (const [itemName, requiredQuantity] of Object.entries(choice.inventoryRequirements)) {
const currentQuantity = inventory[itemName] || 0;
if (currentQuantity < requiredQuantity) {
missingItems.push(`${itemName} (need ${requiredQuantity}, have ${currentQuantity})`);
}
}
if (missingItems.length > 0) {
return {
isValid: false,
reason: `Insufficient inventory: ${missingItems.join(', ')}`,
failedConditions: missingItems,
metadata: {
currentInventory: inventory,
requirements: choice.inventoryRequirements
}
};
}
}
return {
isValid: true,
reason: undefined
};
}
};
}
exports.StandardValidationRules = StandardValidationRules;
/**
* Default implementation of ChoiceValidator
* Uses a rule-based system for extensible validation logic
*/
class DefaultChoiceValidator {
rules = [];
constructor() {
// Add standard validation rules by default
this.addRule(StandardValidationRules.CHOICE_EXISTS);
this.addRule(StandardValidationRules.FLAG_CONDITIONS);
this.addRule(StandardValidationRules.CHOICE_ENABLED);
this.addRule(StandardValidationRules.TIME_CONDITIONS);
this.addRule(StandardValidationRules.INVENTORY_CONDITIONS);
}
validate(choice, context) {
// Run all validation rules in priority order
const sortedRules = this.getRules();
for (const rule of sortedRules) {
const result = rule.validate(choice, context);
// If any rule fails, return that failure
if (!result.isValid) {
return {
...result,
metadata: {
...result.metadata,
failedRule: rule.name,
rulesPassed: sortedRules.indexOf(rule)
}
};
}
}
// All rules passed
return {
isValid: true,
metadata: {
rulesChecked: sortedRules.length,
allRulesPassed: true
}
};
}
getAvailableChoices(context) {
// Use the availableChoices from context (which may have been pre-filtered by conditions)
// instead of all choices from the node
return context.availableChoices.filter(choice => {
const result = this.validate(choice, context);
return result.isValid;
});
}
addRule(rule) {
// Remove any existing rule with the same name
this.removeRule(rule.name);
// Add the new rule
this.rules.push(rule);
// Sort by priority
this.rules.sort((a, b) => a.priority - b.priority);
}
removeRule(ruleName) {
this.rules = this.rules.filter(rule => rule.name !== ruleName);
}
getRules() {
return [...this.rules].sort((a, b) => a.priority - b.priority);
}
}
exports.DefaultChoiceValidator = DefaultChoiceValidator;
/**
* Factory function to create a choice validator with default rules
*/
function createChoiceValidator() {
return new DefaultChoiceValidator();
}
/**
* Utility function to create validation context from engine state
*/
function createValidationContext(currentNode, state, availableChoices, metadata) {
return {
currentNode,
state,
availableChoices,
timestamp: performance.now(),
metadata
};
}
//# sourceMappingURL=validation.js.map