UNPKG

qnce-engine

Version:

Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization

1,243 lines (1,242 loc) 63.5 kB
"use strict"; // QNCE Core Engine - Framework Agnostic // Quantum Narrative Convergence Engine Object.defineProperty(exports, "__esModule", { value: true }); exports.QNCEEngine = exports.ChoiceValidationError = exports.QNCENavigationError = void 0; exports.createQNCEEngine = createQNCEEngine; exports.loadStoryData = loadStoryData; const ObjectPool_1 = require("../performance/ObjectPool"); const ThreadPool_1 = require("../performance/ThreadPool"); const PerfReporter_1 = require("../performance/PerfReporter"); // Branching system imports const branching_1 = require("../narrative/branching"); // Choice validation system - Sprint 3.2 const validation_1 = require("./validation"); const errors_1 = require("./errors"); // Re-export error classes for backward compatibility var errors_2 = require("./errors"); Object.defineProperty(exports, "QNCENavigationError", { enumerable: true, get: function () { return errors_2.QNCENavigationError; } }); Object.defineProperty(exports, "ChoiceValidationError", { enumerable: true, get: function () { return errors_2.ChoiceValidationError; } }); // State persistence imports - Sprint 3.3 const types_1 = require("./types"); // Conditional choice evaluator - Sprint 3.4 const condition_1 = require("./condition"); // Demo narrative data moved to demo-story.ts function findNode(nodes, id) { const node = nodes.find(n => n.id === id); if (!node) throw new Error(`Node not found: ${id}`); return node; } /** * QNCE Engine - Core narrative state management * Framework agnostic implementation with object pooling integration */ class QNCEEngine { state; storyData; // Made public for hot-reload compatibility activeFlowEvents = []; performanceMode = false; enableProfiling = false; branchingEngine; choiceValidator; // Sprint 3.2: Choice validation // Sprint 3.3: State persistence and checkpoints checkpoints = new Map(); maxCheckpoints = 50; autoCheckpointEnabled = false; // Sprint 3.3: State persistence properties checkpointManager; // Sprint 3.5: Autosave and Undo/Redo properties undoStack = []; redoStack = []; autosaveConfig = { enabled: false, triggers: ['choice', 'flag-change'], maxEntries: 10, throttleMs: 1000, includeMetadata: true }; undoRedoConfig = { enabled: true, maxUndoEntries: 50, maxRedoEntries: 20, trackFlagChanges: true, trackChoiceText: true, trackActions: ['choice', 'flag-change', 'state-load'] }; lastAutosaveTime = 0; isUndoRedoOperation = false; storageAdapter; // Sprint 4.1: Telemetry support telemetry; defaultTelemetryCtx; get flags() { return this.state.flags; } get history() { return this.state.history; } get isComplete() { try { return this.getCurrentNode().choices.length === 0; } catch { return true; // If current node not found, consider it complete } } constructor(storyData, initialState, performanceMode = false, threadPoolConfig, options) { this.storyData = storyData; this.performanceMode = performanceMode; this.enableProfiling = performanceMode; // Enable profiling with performance mode // Initialize choice validator (Sprint 3.2) this.choiceValidator = (0, validation_1.createChoiceValidator)(); // Initialize ThreadPool if in performance mode if (this.performanceMode && threadPoolConfig) { (0, ThreadPool_1.getThreadPool)(threadPoolConfig); } this.state = { currentNodeId: initialState?.currentNodeId || storyData.initialNodeId, flags: initialState?.flags || {}, history: initialState?.history || [storyData.initialNodeId], }; // Telemetry wiring this.telemetry = options?.telemetry; if (this.telemetry) { const sessionId = options?.sessionId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; this.defaultTelemetryCtx = { sessionId, storyId: undefined, appVersion: options?.appVersion, engineVersion: types_1.PERSISTENCE_VERSION, env: options?.env }; try { this.telemetry.emit({ type: 'session.start', payload: { initialNodeId: this.state.currentNodeId }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } } } // Lane B: StorageAdapter integration /** Attach a storage adapter for persistence */ attachStorageAdapter(adapter) { this.storageAdapter = adapter; } /** Save current state through the attached storage adapter */ async saveToStorage(key, options = {}) { if (!this.storageAdapter) return { success: false, error: 'No storage adapter attached' }; const t0 = Date.now(); const serialized = await this.saveState(options); const res = await this.storageAdapter.save(key, serialized, options); try { this.telemetry?.emit({ type: 'storage.op', payload: { op: 'save', key, ms: Date.now() - t0, ok: !!res?.success }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return res; } /** Load state from the attached storage adapter */ async loadFromStorage(key, options = {}) { if (!this.storageAdapter) return { success: false, error: 'No storage adapter attached' }; const t0 = Date.now(); const serialized = await this.storageAdapter.load(key, options); if (!serialized) return { success: false, error: `No data for key: ${key}` }; const res = await this.loadState(serialized, options); try { this.telemetry?.emit({ type: 'storage.op', payload: { op: 'load', key, ms: Date.now() - t0, ok: !!res?.success }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return res; } /** Delete a stored state via the adapter */ async deleteFromStorage(key) { if (!this.storageAdapter) return false; const t0 = Date.now(); const ok = await this.storageAdapter.delete(key); try { this.telemetry?.emit({ type: 'storage.op', payload: { op: 'delete', key, ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return ok; } /** List keys from the adapter */ async listStorageKeys() { if (!this.storageAdapter) return []; const t0 = Date.now(); const keys = await this.storageAdapter.list(); try { this.telemetry?.emit({ type: 'storage.op', payload: { op: 'list', count: keys.length, ms: Date.now() - t0, ok: true }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return keys; } /** Check if a key exists via the adapter */ async storageExists(key) { if (!this.storageAdapter) return false; return this.storageAdapter.exists(key); } /** Clear all stored data via the adapter */ async clearStorage() { if (!this.storageAdapter) return false; const t0 = Date.now(); const ok = await this.storageAdapter.clear(); try { this.telemetry?.emit({ type: 'storage.op', payload: { op: 'clear', ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return ok; } /** Get storage statistics from the adapter */ async getStorageStats() { if (!this.storageAdapter) return null; return this.storageAdapter.getStats(); } // Sprint 3.1 - API Consistency Methods /** * Navigate directly to a specific node by ID * @param nodeId - The ID of the node to navigate to * @throws {QNCENavigationError} When nodeId is invalid or not found */ goToNodeById(nodeId) { // Validate node exists const targetNode = this.storyData.nodes.find(n => n.id === nodeId); if (!targetNode) { throw new errors_1.QNCENavigationError(`Node not found: ${nodeId}`); } // Performance profiling for direct navigation const navigationSpanId = this.enableProfiling ? (0, PerfReporter_1.getPerfReporter)().startSpan('custom', { fromNodeId: this.state.currentNodeId, toNodeId: nodeId, navigationType: 'direct' }) : null; const fromNodeId = this.state.currentNodeId; // Update state this.state.currentNodeId = nodeId; this.state.history.push(nodeId); // Record navigation event for analytics if (this.performanceMode) { const flowEvent = this.createFlowEvent(fromNodeId, nodeId, { navigationType: 'direct' }); this.recordFlowEvent(flowEvent); ObjectPool_1.poolManager.returnFlow(flowEvent); } // End profiling span if (navigationSpanId && this.enableProfiling) { (0, PerfReporter_1.getPerfReporter)().endSpan(navigationSpanId, { success: true, targetNodeId: nodeId }); } } /** * Get the current narrative node * @returns The current node object */ getCurrentNode() { const cacheKey = `node-${this.state.currentNodeId}`; // Profiling: Record cache operation if (this.enableProfiling) { const found = findNode(this.storyData.nodes, this.state.currentNodeId); PerfReporter_1.perf.cacheHit(cacheKey, { nodeId: this.state.currentNodeId }); return found; } if (this.performanceMode) { // Use pooled node for enhanced node data const pooledNode = ObjectPool_1.poolManager.borrowNode(); const coreNode = findNode(this.storyData.nodes, this.state.currentNodeId); pooledNode.initialize(coreNode.id, coreNode.text, coreNode.choices); // Return the pooled node (caller should return it when done) return { id: pooledNode.id, text: pooledNode.text, choices: pooledNode.choices }; } const node = findNode(this.storyData.nodes, this.state.currentNodeId); try { this.telemetry?.emit({ type: 'node.enter', payload: { nodeId: node.id }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return node; } /** * Get available choices from the current node with validation and conditional filtering * @returns Array of available choices */ getAvailableChoices() { const currentNode = this.getCurrentNode(); const context = { state: this.state, timestamp: Date.now(), customData: {} }; // First apply conditional filtering (Sprint 3.4) const conditionallyAvailable = currentNode.choices.filter((choice) => { // If no condition is specified, choice is always available if (!choice.condition) { return true; } try { // Evaluate the condition using the condition evaluator const t0 = Date.now(); const res = condition_1.conditionEvaluator.evaluate(choice.condition, context); try { this.telemetry?.emit({ type: 'expression.evaluate', payload: { ok: true, ms: Date.now() - t0 }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } return res; } catch (error) { // Log condition evaluation errors but don't block other choices if (error instanceof condition_1.ConditionEvaluationError) { console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, { choiceText: choice.text, condition: choice.condition, nodeId: this.state.currentNodeId }); try { this.telemetry?.emit({ type: 'expression.evaluate', payload: { ok: false, error: 'ConditionEvaluationError' }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } } else { console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error); try { this.telemetry?.emit({ type: 'engine.error', payload: { where: 'getAvailableChoices', error: error?.message || String(error) }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } } // Return false for invalid conditions (choice won't be shown) return false; } }); // Then apply choice validation (Sprint 3.2) const validationContext = (0, validation_1.createValidationContext)(currentNode, this.state, conditionallyAvailable // Pass the conditionally filtered choices ); return this.choiceValidator.getAvailableChoices(validationContext); } makeChoice(choiceIndex) { const choices = this.getAvailableChoices(); if (choiceIndex < 0 || choiceIndex >= choices.length) { throw new errors_1.QNCENavigationError(`Invalid choice index: ${choiceIndex}. Please select a number between 1 and ${choices.length}.`); } const selectedChoice = choices[choiceIndex]; // Sprint 3.2: Validate choice before execution const currentNode = this.getCurrentNode(); const context = (0, validation_1.createValidationContext)(currentNode, this.state, choices); const validationResult = this.choiceValidator.validate(selectedChoice, context); if (!validationResult.isValid) { throw new errors_1.ChoiceValidationError(selectedChoice, validationResult, choices); } // Execute the validated choice this.selectChoice(selectedChoice); } getState() { return { ...this.state }; } getFlags() { return { ...this.state.flags }; } setFlag(key, value) { // Sprint 3.5: Save state for undo before making changes const preChangeState = this.deepCopy(this.state); this.state.flags[key] = value; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('flag-change')) { this.pushToUndoStack(preChangeState, 'flag-change', { flagsChanged: [key], flagValue: value }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('flag-change')) { this.triggerAutosave('flag-change', { flagKey: key, flagValue: value }).catch((error) => { console.warn('[QNCE] Autosave failed:', error.message); }); } } getHistory() { return [...this.state.history]; } selectChoice(choice) { // Sprint 3.5: Save state for undo before making changes const preChangeState = this.deepCopy(this.state); // S2-T4: Add state transition profiling const transitionSpanId = this.enableProfiling ? (0, PerfReporter_1.getPerfReporter)().startSpan('state-transition', { fromNodeId: this.state.currentNodeId, toNodeId: choice.nextNodeId, hasEffects: !!choice.flagEffects }) : null; const fromNodeId = this.state.currentNodeId; const toNodeId = choice.nextNodeId; // Telemetry: choice.select try { this.telemetry?.emit({ type: 'choice.select', payload: { fromNodeId, toNodeId, choiceText: choice.text }, ts: Date.now(), ctx: this.defaultTelemetryCtx }); } catch { } // Create flow event for tracking narrative progression if (this.performanceMode) { const flowSpanId = this.enableProfiling ? PerfReporter_1.perf.flowStart(fromNodeId, { toNodeId }) : null; const flowEvent = this.createFlowEvent(fromNodeId, toNodeId, choice.flagEffects); this.recordFlowEvent(flowEvent); // Return the flow immediately after recording (we don't need to keep it) // This ensures proper pool recycling ObjectPool_1.poolManager.returnFlow(flowEvent); if (flowSpanId) { PerfReporter_1.perf.flowComplete(flowSpanId, toNodeId, { transitionType: 'choice' }); } } this.state.currentNodeId = choice.nextNodeId; this.state.history.push(choice.nextNodeId); if (choice.flagEffects) { this.state.flags = { ...this.state.flags, ...choice.flagEffects }; // S2-T4: Track flag updates if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { flagCount: Object.keys(choice.flagEffects).length, nodeId: toNodeId, eventType: 'flag-update' }); } } // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('choice')) { this.pushToUndoStack(preChangeState, 'choice', { nodeId: fromNodeId, choiceText: this.undoRedoConfig.trackChoiceText ? choice.text : undefined, flagsChanged: choice.flagEffects ? Object.keys(choice.flagEffects) : undefined }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('choice')) { this.triggerAutosave('choice', { fromNodeId, toNodeId, choiceText: choice.text }).catch(error => { console.warn('[QNCE] Autosave failed:', error.message); }); } // Complete state transition span if (transitionSpanId) { (0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, { historyLength: this.state.history.length, flagCount: Object.keys(this.state.flags).length }); } // S2-T2: Background operations after state change if (this.performanceMode) { // Preload next possible nodes in background this.preloadNextNodes().catch(error => { console.warn('[QNCE] Background preload failed:', error.message); }); // Write telemetry data in background this.backgroundTelemetryWrite({ action: 'choice-selected', fromNodeId, toNodeId, choiceText: choice.text.slice(0, 50) // First 50 chars for privacy }).catch(error => { console.warn('[QNCE] Background telemetry failed:', error.message); }); } } /** Flush telemetry (useful before process exit) */ async flushTelemetry() { try { await this.telemetry?.flush(); } catch { } } resetNarrative() { // Sprint 3.5: Save state for undo before reset const preChangeState = this.deepCopy(this.state); // Clean up pooled objects before reset if (this.performanceMode) { this.cleanupPools(); } this.state.currentNodeId = this.storyData.initialNodeId; this.state.flags = {}; this.state.history = [this.storyData.initialNodeId]; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('reset')) { this.pushToUndoStack(preChangeState, 'reset', { nodeId: this.storyData.initialNodeId }); } } /** * Load simple state (legacy method for backward compatibility) * @param state - QNCEState to load */ loadSimpleState(state) { // Sprint 3.5: Save state for undo before loading const preChangeState = this.deepCopy(this.state); this.state = { ...state }; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('state-load')) { this.pushToUndoStack(preChangeState, 'state-load', { nodeId: state.currentNodeId }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('state-load')) { this.triggerAutosave('state-load', { nodeId: state.currentNodeId }).catch((error) => { console.warn('[QNCE] Autosave failed:', error.message); }); } } // Sprint 3.3: State Persistence & Checkpoints Implementation /** * Save current engine state to a serialized format * @param options - Serialization options * @returns Promise resolving to serialized state */ async saveState(options = {}) { const startTime = performance.now(); if (!this.state.currentNodeId) { throw new Error('Invalid state: currentNodeId is missing.'); } try { // Create serialization metadata const metadata = { engineVersion: types_1.PERSISTENCE_VERSION, timestamp: new Date().toISOString(), storyId: this.generateStoryHash(), customMetadata: options.customMetadata || {}, compression: options.compression || 'none' }; // Build serialized state const serializedState = { state: this.deepCopy(this.state), flowEvents: options.includeFlowEvents !== false ? this.deepCopy(this.activeFlowEvents) : [], metadata }; // Add optional data based on options if (options.includePerformanceData && this.performanceMode) { serializedState.performanceState = { performanceMode: this.performanceMode, enableProfiling: this.enableProfiling, backgroundTasks: [], // Would be populated with actual background task IDs telemetryData: [] // Would be populated with telemetry history }; serializedState.poolStats = this.getPoolStats() || {}; } if (options.includeBranchingContext && this.branchingEngine) { serializedState.branchingContext = { activeBranches: [], // Would be populated from branching engine branchStates: {}, convergencePoints: [] }; } if (options.includeValidationState) { serializedState.validationState = { disabledRules: [], customRules: {}, validationErrors: [] }; } // Generate checksum if requested if (options.generateChecksum) { const stateToHash = { ...serializedState }; delete stateToHash.metadata.checksum; // Exclude checksum from hash const stateString = JSON.stringify(stateToHash); metadata.checksum = await this.generateChecksum(stateString); serializedState.metadata = metadata; } // Performance tracking if (this.enableProfiling) { const duration = performance.now() - startTime; PerfReporter_1.perf.record('custom', { eventType: 'state-serialized', duration, stateSize: JSON.stringify(serializedState).length, includePerformance: !!options.includePerformanceData, includeFlowEvents: options.includeFlowEvents !== false }); } return serializedState; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown serialization error'; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-serialization-failed', error: errorMessage, duration: performance.now() - startTime }); } throw new Error(`Failed to save state: ${errorMessage}`); } } /** * Load engine state from serialized format * @param serializedState - The serialized state to load * @param options - Load options * @returns Promise resolving to persistence result */ async loadState(serializedState, options = {}) { const startTime = performance.now(); try { // Validate serialized state structure const validationResult = this.validateSerializedState(serializedState); if (!validationResult.isValid) { return { success: false, error: `Invalid serialized state: ${validationResult.errors.join(', ')}`, warnings: validationResult.warnings }; } // Check compatibility if (!options.skipCompatibilityCheck) { const compatibilityCheck = this.checkCompatibility(serializedState.metadata); if (!compatibilityCheck.compatible) { return { success: false, error: `Incompatible state version: ${compatibilityCheck.reason}`, warnings: compatibilityCheck.suggestions }; } } // Verify checksum if available and requested if (options.verifyChecksum && serializedState.metadata.checksum) { const isValid = await this.verifyChecksum(serializedState); if (!isValid) { return { success: false, error: 'Checksum verification failed - state may be corrupted' }; } } // Story hash validation if (this.generateStoryHash() !== serializedState.metadata.storyId) { return { success: false, error: 'Story hash mismatch. The state belongs to a different narrative.' }; } // Apply migration if needed let stateToLoad = serializedState; if (options.migrationFunction) { stateToLoad = options.migrationFunction(serializedState); } // Load core state this.state = this.deepCopy(stateToLoad.state); // Restore optional data based on options if (options.restoreFlowEvents && stateToLoad.flowEvents) { this.activeFlowEvents = this.deepCopy(stateToLoad.flowEvents); } if (options.restorePerformanceState && stateToLoad.performanceState) { this.performanceMode = stateToLoad.performanceState.performanceMode; this.enableProfiling = stateToLoad.performanceState.enableProfiling; // Background tasks would be restored here } if (options.restoreBranchingContext && stateToLoad.branchingContext && this.branchingEngine) { // Restore branching context - would be implemented with actual branching engine } // Performance tracking const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-loaded', duration, stateSize: JSON.stringify(stateToLoad).length, restoredFlowEvents: !!options.restoreFlowEvents, restoredPerformance: !!options.restorePerformanceState }); } return { success: true, data: { size: JSON.stringify(stateToLoad).length, duration, checksum: stateToLoad.metadata.checksum } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown loading error'; const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-loading-failed', error: errorMessage, duration }); } return { success: false, error: `Failed to load state: ${errorMessage}`, data: { duration } }; } } /** * Create a lightweight checkpoint of current state * @param name - Optional checkpoint name * @param options - Checkpoint options * @returns Promise resolving to created checkpoint */ async createCheckpoint(name, options = {}) { const checkpointId = this.generateCheckpointId(); const timestamp = new Date().toISOString(); const checkpoint = { id: checkpointId, name: name || `Checkpoint ${checkpointId.slice(-8)}`, state: this.deepCopy(this.state), timestamp, description: options.includeMetadata ? `Auto-checkpoint at node ${this.state.currentNodeId}` : undefined, tags: options.autoTags || [], metadata: options.includeMetadata ? { nodeTitle: this.getCurrentNode().text.slice(0, 50), choiceCount: this.getCurrentNode().choices.length, flagCount: Object.keys(this.state.flags).length, historyLength: this.state.history.length } : undefined }; // Store checkpoint this.checkpoints.set(checkpointId, checkpoint); // Cleanup old checkpoints if needed const maxCheckpoints = options.maxCheckpoints || this.maxCheckpoints; if (this.checkpoints.size > maxCheckpoints) { await this.cleanupCheckpoints(options.cleanupStrategy || 'lru', maxCheckpoints); } // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-created', checkpointId, checkpointCount: this.checkpoints.size, stateSize: JSON.stringify(checkpoint.state).length }); } return checkpoint; } /** * Restore engine state from a checkpoint * @param checkpointId - ID of checkpoint to restore * @returns Promise resolving to persistence result */ async restoreFromCheckpoint(checkpointId) { const startTime = performance.now(); try { const checkpoint = this.checkpoints.get(checkpointId); if (!checkpoint) { return { success: false, error: `Checkpoint not found: ${checkpointId}` }; } // Restore state this.state = this.deepCopy(checkpoint.state); const duration = performance.now() - startTime; // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-restored', checkpointId, duration, stateSize: JSON.stringify(checkpoint.state).length }); } return { success: true, data: { size: JSON.stringify(checkpoint.state).length, duration, checksum: checkpointId } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown restore error'; const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-restore-failed', checkpointId, error: errorMessage, duration }); } return { success: false, error: `Failed to restore checkpoint: ${errorMessage}`, data: { duration } }; } } // Utility method for checking flag conditions checkFlag(flagName, expectedValue) { if (expectedValue === undefined) { return this.state.flags[flagName] !== undefined; } return this.state.flags[flagName] === expectedValue; } // Sprint 3.2: Choice Validation Methods /** * Get the choice validator instance * @returns The current choice validator */ getChoiceValidator() { return this.choiceValidator; } /** * Validate a specific choice without executing it * @param choice - The choice to validate * @returns Validation result with details */ validateChoice(choice) { const currentNode = this.getCurrentNode(); const availableChoices = this.getAvailableChoices(); const context = (0, validation_1.createValidationContext)(currentNode, this.state, availableChoices); return this.choiceValidator.validate(choice, context); } /** * Check if a specific choice is valid without executing it * @param choice - The choice to check * @returns True if the choice is valid, false otherwise */ isChoiceValid(choice) { const result = this.validateChoice(choice); return result.isValid; } // Performance and object pooling methods /** * Create a flow event using pooled objects for performance tracking */ createFlowEvent(fromNodeId, toNodeId, metadata) { const flow = ObjectPool_1.poolManager.borrowFlow(); flow.initialize(fromNodeId, metadata); flow.addTransition(fromNodeId, toNodeId); return flow; } /** * Record and manage flow events */ recordFlowEvent(flow) { const flowEvent = { id: `${flow.nodeId}-${Date.now()}`, fromNodeId: flow.nodeId, toNodeId: flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '', timestamp: flow.timestamp, metadata: flow.metadata }; this.activeFlowEvents.push(flowEvent); // Clean up old flow events (basic LRU-style cleanup) if (this.activeFlowEvents.length > 10) { this.activeFlowEvents.shift(); // Remove oldest } } /** * Get current flow events (for debugging/monitoring) */ getActiveFlows() { return [...this.activeFlowEvents]; } /** * Get object pool statistics for performance monitoring */ getPoolStats() { return this.performanceMode ? ObjectPool_1.poolManager.getAllStats() : null; } /** * Return all pooled objects (cleanup method) */ cleanupPools() { // Clear flow events (no pooled objects to return since we return them immediately) this.activeFlowEvents.length = 0; } // S2-T2: Background ThreadPool Operations /** * Preload next possible nodes in background using ThreadPool */ async preloadNextNodes(choice) { if (!this.performanceMode) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); const currentNode = this.getCurrentNode(); const choicesToPreload = choice ? [choice] : currentNode.choices; // Submit background jobs for each node to preload for (const ch of choicesToPreload) { threadPool.submitJob('cache-load', { nodeId: ch.nextNodeId }, 'normal').catch(error => { if (this.enableProfiling) { PerfReporter_1.perf.record('cache-miss', { nodeId: ch.nextNodeId, error: error.message, jobType: 'preload' }); } }); } } /** * Write telemetry data in background using ThreadPool */ async backgroundTelemetryWrite(eventData) { if (!this.performanceMode || !this.enableProfiling) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); const telemetryData = { timestamp: performance.now(), sessionId: this.state.history[0], // Use first node as session ID eventData, stateSnapshot: { nodeId: this.state.currentNodeId, flagCount: Object.keys(this.state.flags).length, historyLength: this.state.history.length } }; threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => { console.warn('[QNCE] Telemetry write failed:', error.message); }); } // ================================ // Sprint #3: Advanced Branching System Integration // ================================ /** * Enable advanced branching capabilities for this story * Integrates the QNCE Branching API with the core engine */ enableBranching(story) { if (this.branchingEngine) { console.warn('[QNCE] Branching already enabled for this engine instance'); return this.branchingEngine; } // Create branching engine with current state this.branchingEngine = (0, branching_1.createBranchingEngine)(story, this.state); if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-enabled', storyId: story.id, chapterCount: story.chapters.length, performanceMode: this.performanceMode }); } return this.branchingEngine; } /** * Get the branching engine if enabled */ getBranchingEngine() { return this.branchingEngine; } /** * Check if branching is enabled */ isBranchingEnabled() { return !!this.branchingEngine; } /** * Sync core engine state with branching engine * Call this when core state changes to keep branching engine updated */ syncBranchingState() { if (this.branchingEngine) { // The branching engine maintains its own state copy // This method could be extended to sync state changes if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-state-synced', currentNodeId: this.state.currentNodeId }); } } } /** * Disable branching and cleanup resources */ disableBranching() { if (this.branchingEngine) { this.branchingEngine = undefined; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-disabled' }); } } } /** * Background cache warming for story data */ async warmCache() { if (!this.performanceMode) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); // Cache all nodes in background const cacheWarmData = { nodeIds: this.storyData.nodes.map(n => n.id), storyId: this.storyData.initialNodeId }; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'cache-warm-start', nodeCount: this.storyData.nodes.length }); } threadPool.submitJob('cache-load', cacheWarmData, 'low').catch(error => { if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'cache-warm-failed', error: error.message }); } }); } // Sprint 3.3: State persistence utility methods /** * Deep copy utility for state objects * @param obj - Object to deep copy * @returns Deep copied object */ deepCopy(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map(item => this.deepCopy(item)); } if (typeof obj === 'object') { const copy = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { copy[key] = this.deepCopy(obj[key]); } } return copy; } return obj; } /** * Generate a hash for the current story data * @returns Story hash string */ generateStoryHash() { const storyString = JSON.stringify({ initialNodeId: this.storyData.initialNodeId, nodeCount: this.storyData.nodes.length, nodeIds: this.storyData.nodes.map(n => n.id).sort() }); // Simple hash function (in production, use crypto.subtle.digest) let hash = 0; for (let i = 0; i < storyString.length; i++) { const char = storyString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } /** * Generate a unique checkpoint ID * @returns Unique checkpoint ID */ generateCheckpointId() { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `checkpoint_${timestamp}_${random}`; } /** * Generate checksum for data integrity * @param data - Data to generate checksum for * @returns Promise resolving to checksum string */ async generateChecksum(data) { // Simple checksum implementation (in production, use crypto.subtle.digest) let checksum = 0; for (let i = 0; i < data.length; i++) { checksum = ((checksum << 5) - checksum) + data.charCodeAt(i); checksum = checksum & checksum; } return checksum.toString(16); } /** * Verify checksum integrity * @param serializedState - State with checksum to verify * @returns Promise resolving to verification result */ async verifyChecksum(serializedState) { const receivedChecksum = serializedState.metadata.checksum; if (!receivedChecksum) return false; const stateToHash = { ...serializedState }; delete stateToHash.metadata.checksum; const stateString = JSON.stringify(stateToHash); const expectedChecksum = await this.generateChecksum(stateString); return receivedChecksum === expectedChecksum; } /** * Validate the structure of the serialized state object * @param serializedState - State to validate * @returns Validation result */ validateSerializedState(serializedState) { const errors = []; const warnings = []; // Check required fields if (!serializedState.state) { errors.push('Missing state field'); } else { if (!serializedState.state.currentNodeId) { errors.push('Missing currentNodeId in state'); } if (!serializedState.state.flags) { warnings.push('Missing flags in state'); } if (!serializedState.state.history) { warnings.push('Missing history in state'); } } if (!serializedState.metadata) { errors.push('Missing metadata field'); } else { if (!serializedState.metadata.engineVersion) { warnings.push('Missing engine version in metadata'); } if (!serializedState.metadata.timestamp) { warnings.push('Missing timestamp in metadata'); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Check version compatibility * @param metadata - Serialization metadata * @returns Compatibility check result */ checkCompatibility(metadata) { const currentVersion = types_1.PERSISTENCE_VERSION; const stateVersion = metadata.engineVersion; if (!stateVersion) { return { compatible: false, reason: 'Unknown state version', suggestions: ['State may be from an older engine version'] }; } if (stateVersion === currentVersion) { return { compatible: true }; } // Simple version comparison (in production, use semver) const [currentMajor, currentMinor] = currentVersion.split('.').map(Number); const [stateMajor, stateMinor] = stateVersion.split('.').map(Number); if (stateMajor > currentMajor || (stateMajor === currentMajor && stateMinor > currentMinor)) { return { compatible: false, reason: `State from newer engine version (${stateVersion} > ${currentVersion})`, suggestions: ['Update the engine to a newer version'] }; } if (stateMajor < currentMajor) { return { compatible: false, reason: `State from incompatible major version (${stateVersion} vs ${currentVersion})`, suggestions: ['Use migration function to upgrade state format'] }; } // Minor version differences are usually compatible return { compatible: true, suggestions: [`State from older minor version (${stateVersion})`] }; } /** * Cleanup old checkpoints based on strategy * @param strategy - Cleanup strategy * @param maxCheckpoints - Maximum number of checkpoints to keep * @returns Promise resolving to number of cleaned checkpoints */ async cleanupCheckpoints(strategy, maxCheckpoints) { const checkpoints = Array.from(this.checkpoints.entries()); if (checkpoints.length <= maxCheckpoints) { return 0; } const toRemove = checkpoints.length - maxCheckpoints; let checkpointsToDelete = []; switch (strategy) { case 'fifo': // Remove oldest by creation order (assuming IDs contain timestamp) checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; case 'timestamp': // Remove oldest by timestamp checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; case 'lru': default: // For now, same as FIFO (would need access tracking in production) checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; } // Remove checkpoints for (const id of checkpointsToDelete) { this.checkpoints.delete(id); } if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoints-cleaned', strategy, removedCount: checkpointsToDelete.length, remainingCount: this.checkpoints.size }); } return checkpointsToDelete.length; } /** * Get all checkpoints * @returns Array of all checkpoints */ getCheckpoints() { return Array.from(this.checkpoints.values()); } /** * Get specific checkpoint by ID * @param id - Checkpoint ID * @returns Checkpoint or undefined */ getCheckpoint(id) { return this.checkpoints.get(id); } /** * Delete a checkpoint * @param id - Checkpoint ID to delete * @returns Whether checkpoint was deleted */ deleteCheckpoint(id) {